Files
wendeIT-scripting/Intune/Intune-App-Assignment-Application-v5_2_0_MW.ps1
T
2026-06-17 07:17:35 +02:00

4019 lines
174 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Intune App-Zuweisungs-Manager - Erweiterte Version (v5.2.0 - Gruppenabfrage hinzugefügt)
# Mit App-Suche, Filter, Required-Gruppen erstellen, Session-Übersicht, RPA-Tab, Gruppenabfrage und target-spezifischen Zuweisungen
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Windows.Forms
# Funktion zum Laden des Microsoft.Graph.Authentication Moduls (wird beim Connect aufgerufen)
# NUR dieses Modul wird benötigt - alle anderen Aufrufe gehen über Invoke-MgGraphRequest
function Initialize-GraphModules {
if (Get-Module Microsoft.Graph.Authentication) {
return $true
}
try {
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
return $true
} catch {
[System.Windows.MessageBox]::Show(
"Fehler beim Laden von Microsoft.Graph.Authentication:`n$($_.Exception.Message)`n`nBitte führen Sie aus:`nInstall-Module Microsoft.Graph.Authentication -Force",
"Modul-Fehler", "OK", "Error")
return $false
}
}
# ===== GRAPH API HILFSFUNKTIONEN =====
# Diese ersetzen die Microsoft.Graph.* Cmdlets um Versionskonflikte zu vermeiden
function Get-GraphGroup {
param(
[string]$GroupId,
[string]$Filter,
[string[]]$Property,
[switch]$All
)
try {
$select = if ($Property) { "&`$select=" + ($Property -join ",").ToLower() } else { "" }
if ($GroupId) {
$uri = "https://graph.microsoft.com/v1.0/groups/$GroupId`?`$select=" + ($Property -join ",").ToLower()
return Invoke-MgGraphRequest -Uri $uri -Method GET
}
else {
$uri = "https://graph.microsoft.com/v1.0/groups"
if ($Filter) { $uri += "?`$filter=$Filter$select" }
elseif ($select) { $uri += "?$($select.TrimStart('&'))" }
$results = @()
do {
$response = Invoke-MgGraphRequest -Uri $uri -Method GET
if ($response.value) { $results += $response.value }
$uri = $response.'@odata.nextLink'
} while ($All -and $uri)
return $results
}
} catch {
Write-Warning "Get-GraphGroup Fehler: $_"
return $null
}
}
function Get-GraphGroupMember {
param([string]$GroupId, [switch]$All)
try {
$uri = "https://graph.microsoft.com/v1.0/groups/$GroupId/members"
$results = @()
do {
$response = Invoke-MgGraphRequest -Uri $uri -Method GET
if ($response.value) { $results += $response.value }
$uri = $response.'@odata.nextLink'
} while ($All -and $uri)
return $results
} catch {
Write-Warning "Get-GraphGroupMember Fehler: $_"
return @()
}
}
function Find-GraphGroupsByName {
# Sucht beliebige Gruppen im Tenant per Teilstring (nicht nur Präfix wie Get-GraphGroup -Filter startswith)
param([string]$SearchTerm)
try {
$encoded = $SearchTerm.Replace("'", "''")
$uri = "https://graph.microsoft.com/v1.0/groups?`$filter=contains(displayName,'$encoded')&`$count=true&`$top=999&`$select=id,displayName"
$results = @()
do {
$response = Invoke-MgGraphRequest -Uri $uri -Method GET -Headers @{ ConsistencyLevel = "eventual" }
if ($response.value) { $results += $response.value }
$uri = $response.'@odata.nextLink'
} while ($uri)
return $results
} catch {
Write-Warning "Find-GraphGroupsByName Fehler: $_"
return @()
}
}
function Get-ResolvedGroupMembers {
# Liest die Mitglieder einer Gruppe aus (User, Geräte, ggf. verschachtelte Gruppen)
param(
[string]$GroupId,
[string]$SourceGroupName,
[switch]$ResolveNested,
[System.Collections.Generic.HashSet[string]]$Visited
)
if (-not $Visited) { $Visited = [System.Collections.Generic.HashSet[string]]::new() }
if ($Visited.Contains($GroupId)) { return @() }
[void]$Visited.Add($GroupId)
$members = Get-GraphGroupMember -GroupId $GroupId -All
$results = [System.Collections.Generic.List[object]]::new()
foreach ($m in $members) {
$odataType = $m.'@odata.type'
switch ($odataType) {
'#microsoft.graph.user' {
$results.Add([PSCustomObject]@{
DisplayName = $m.displayName
UserPrincipalName = $m.userPrincipalName
Id = $m.id
ObjectType = "Benutzer"
SourceGroup = $SourceGroupName
})
}
'#microsoft.graph.device' {
$results.Add([PSCustomObject]@{
DisplayName = $m.displayName
UserPrincipalName = ""
Id = $m.id
ObjectType = "Gerät"
SourceGroup = $SourceGroupName
})
}
'#microsoft.graph.group' {
if ($ResolveNested) {
$nested = Get-ResolvedGroupMembers -GroupId $m.id -SourceGroupName $m.displayName -ResolveNested -Visited $Visited
foreach ($n in $nested) { $results.Add($n) }
}
else {
$results.Add([PSCustomObject]@{
DisplayName = $m.displayName
UserPrincipalName = ""
Id = $m.id
ObjectType = "Gruppe (nicht aufgelöst)"
SourceGroup = $SourceGroupName
})
}
}
default {
$results.Add([PSCustomObject]@{
DisplayName = $m.displayName
UserPrincipalName = ""
Id = $m.id
ObjectType = "Andere ($odataType)"
SourceGroup = $SourceGroupName
})
}
}
}
return $results
}
function Get-GraphUser {
param(
[string]$UserId,
[string]$Filter,
[string[]]$Property,
[switch]$All
)
try {
$select = if ($Property) { "`$select=" + ($Property -join ",").ToLower() } else { "" }
if ($UserId) {
$uri = "https://graph.microsoft.com/v1.0/users/$UserId"
if ($select) { $uri += "?$select" }
return Invoke-MgGraphRequest -Uri $uri -Method GET
}
else {
$uri = "https://graph.microsoft.com/v1.0/users"
$params = @()
if ($Filter) { $params += "`$filter=$Filter" }
if ($select) { $params += $select }
if ($params) { $uri += "?" + ($params -join "&") }
$results = @()
do {
$response = Invoke-MgGraphRequest -Uri $uri -Method GET
if ($response.value) { $results += $response.value }
$uri = $response.'@odata.nextLink'
} while ($All -and $uri)
return $results
}
} catch {
Write-Warning "Get-GraphUser Fehler: $_"
return $null
}
}
function New-GraphGroup {
param(
[string]$DisplayName,
[bool]$MailEnabled = $false,
[bool]$SecurityEnabled = $true,
[string]$MailNickname
)
$body = @{
displayName = $DisplayName
mailEnabled = $MailEnabled
securityEnabled = $SecurityEnabled
mailNickname = $MailNickname
}
return Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/groups" -Method POST -Body $body
}
function Add-GraphGroupMember {
param([string]$GroupId, [string]$DirectoryObjectId)
$body = @{
"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$DirectoryObjectId"
}
Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId/members/`$ref" -Method POST -Body $body
}
function New-AssignmentReport {
param(
$Targets,
$Assignments,
$IsUserMode,
$SuccessCount,
$ErrorCount,
$SkippedCount,
$DetailedResults
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$dateForFile = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$targetType = if ($IsUserMode) { "Benutzer" } else { "Gruppen/Abteilungen" }
$executedBy = $env:USERNAME
$totalOps = $SuccessCount + $ErrorCount + $SkippedCount
# Gruppiere Assignments nach Typ
$availableApps = @($Assignments | Where-Object { $_.Type -eq "Available" })
$requiredApps = @($Assignments | Where-Object { $_.Type -eq "Required" })
$html = @"
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Intune Zuweisungs-Report - $dateForFile</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; }
.header { background: linear-gradient(135deg, #0078d4, #106ebe); color: white; padding: 30px; }
.header h1 { font-size: 24px; margin-bottom: 5px; }
.header .subtitle { opacity: 0.9; font-size: 14px; }
.meta-info { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; padding: 20px; background: #f8f9fa; border-bottom: 1px solid #e9ecef; }
.meta-item { display: flex; flex-direction: column; }
.meta-label { font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.5px; }
.meta-value { font-size: 16px; font-weight: 600; color: #212529; margin-top: 4px; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; padding: 20px; }
.stat-card { padding: 20px; border-radius: 8px; text-align: center; }
.stat-card.total { background: #e3f2fd; color: #1565c0; }
.stat-card.success { background: #e8f5e9; color: #2e7d32; }
.stat-card.skipped { background: #fff3e0; color: #ef6c00; }
.stat-card.error { background: #ffebee; color: #c62828; }
.stat-number { font-size: 32px; font-weight: 700; }
.stat-label { font-size: 12px; text-transform: uppercase; margin-top: 5px; opacity: 0.8; }
.section { padding: 20px; border-top: 1px solid #e9ecef; }
.section-title { font-size: 18px; font-weight: 600; color: #212529; margin-bottom: 15px; display: flex; align-items: center; gap: 10px; }
.section-title .badge { font-size: 12px; padding: 4px 10px; border-radius: 12px; font-weight: 500; }
.badge-available { background: #e3f2fd; color: #1565c0; }
.badge-required { background: #fce4ec; color: #c2185b; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #e9ecef; }
th { background: #f8f9fa; font-weight: 600; color: #495057; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }
tr:hover { background: #f8f9fa; }
.app-name { font-weight: 500; color: #212529; }
.group-name { color: #6c757d; font-size: 14px; }
.type-badge { display: inline-block; padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: 500; }
.type-available { background: #e3f2fd; color: #1565c0; }
.type-required { background: #fce4ec; color: #c2185b; }
.targets-list { display: flex; flex-wrap: wrap; gap: 8px; }
.target-chip { background: #e9ecef; padding: 6px 12px; border-radius: 16px; font-size: 13px; color: #495057; }
.footer { padding: 20px; background: #f8f9fa; text-align: center; color: #6c757d; font-size: 12px; border-top: 1px solid #e9ecef; }
.no-data { color: #6c757d; font-style: italic; padding: 20px; text-align: center; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Intune App-Zuweisungs-Report</h1>
<div class="subtitle">HanseMerkur | Automatisierte App-Zuweisung</div>
</div>
<div class="meta-info">
<div class="meta-item">
<span class="meta-label">Datum & Uhrzeit</span>
<span class="meta-value">$timestamp</span>
</div>
<div class="meta-item">
<span class="meta-label">Ausgefuhrt von</span>
<span class="meta-value">$executedBy</span>
</div>
<div class="meta-item">
<span class="meta-label">Ziel-Typ</span>
<span class="meta-value">$targetType</span>
</div>
<div class="meta-item">
<span class="meta-label">Anzahl Ziele</span>
<span class="meta-value">$($Targets.Count)</span>
</div>
</div>
<div class="stats">
<div class="stat-card total">
<div class="stat-number">$totalOps</div>
<div class="stat-label">Gesamt</div>
</div>
<div class="stat-card success">
<div class="stat-number">$SuccessCount</div>
<div class="stat-label">Erfolgreich</div>
</div>
<div class="stat-card skipped">
<div class="stat-number">$SkippedCount</div>
<div class="stat-label">Ubersprungen</div>
</div>
<div class="stat-card error">
<div class="stat-number">$ErrorCount</div>
<div class="stat-label">Fehler</div>
</div>
</div>
<div class="section">
<div class="section-title">Zugewiesene $targetType</div>
<div class="targets-list">
"@
foreach ($target in $Targets) {
$html += " <span class=`"target-chip`">$($target.DisplayName)</span>`n"
}
$html += @"
</div>
</div>
<div class="section">
<div class="section-title">
Available-Zuweisungen
<span class="badge badge-available">$($availableApps.Count) Apps</span>
</div>
"@
if ($availableApps.Count -gt 0) {
$html += @"
<table>
<thead>
<tr>
<th>App-Name</th>
<th>Ziel-Gruppe</th>
</tr>
</thead>
<tbody>
"@
foreach ($app in $availableApps) {
$html += " <tr><td class=`"app-name`">$($app.AppName)</td><td class=`"group-name`">$($app.GroupName)</td></tr>`n"
}
$html += " </tbody></table>`n"
} else {
$html += " <div class=`"no-data`">Keine Available-Zuweisungen</div>`n"
}
$html += @"
</div>
<div class="section">
<div class="section-title">
Required-Zuweisungen
<span class="badge badge-required">$($requiredApps.Count) Apps</span>
</div>
"@
if ($requiredApps.Count -gt 0) {
$html += @"
<table>
<thead>
<tr>
<th>App-Name</th>
<th>Ziel-Gruppe</th>
</tr>
</thead>
<tbody>
"@
foreach ($app in $requiredApps) {
$html += " <tr><td class=`"app-name`">$($app.AppName)</td><td class=`"group-name`">$($app.GroupName)</td></tr>`n"
}
$html += " </tbody></table>`n"
} else {
$html += " <div class=`"no-data`">Keine Required-Zuweisungen</div>`n"
}
$html += @"
</div>
<div class="footer">
Report erstellt von Intune App Manager v5.2.0 | WendeIT Marco Wende
</div>
</div>
</body>
</html>
"@
# Report auf Desktop speichern
$desktopPath = [Environment]::GetFolderPath("Desktop")
$reportPath = Join-Path $desktopPath "Intune-Zuweisungs-Report_$dateForFile.html"
$html | Out-File -FilePath $reportPath -Encoding UTF8
return $reportPath
}
$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Intune App Manager (v5.2.0 by WendeIT - Marco Wende)" Height="750" Width="1650">
<Window.Resources>
<!-- Checkbox Style für Zentrierung -->
<Style x:Key="CenteredCheckBox" TargetType="CheckBox">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="*"/>
<RowDefinition Height="80"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="#0da075" Padding="10">
<Grid>
<TextBlock Text="HanseMerkur | Intune App Manager v5.2.0" Foreground="White" FontSize="18"
VerticalAlignment="Center" FontWeight="Bold"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Name="txtStatus" Text="Nicht verbunden" Foreground="White"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<Button Name="btnConnect" Content="Mit Graph verbinden" Width="150" Height="30"/>
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="380"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="320"/>
</Grid.ColumnDefinitions>
<TabControl Grid.Column="0" Margin="10" Name="tabMain">
<TabItem Header="Abteilungsgruppen">
<DockPanel Margin="5">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock Text="Abteilungsgruppen (abt-hm-*)" FontWeight="Bold" FontSize="14" Margin="0,0,0,5"/>
<Grid Margin="0,0,0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Name="txtGroupSearch" Height="25" Grid.Column="0"
Tag="Abteilung suchen..." Margin="0,0,5,0"/>
<Button Name="btnClearGroupSearch" Content="✕" Width="25" Height="25"
Grid.Column="1" ToolTip="Suche zurücksetzen" Margin="0,0,5,0"/>
</Grid>
<Button Name="btnLoadGroups" Content="Gruppen laden" Height="25" Margin="0,0,0,5"/>
<TextBlock Name="txtGroupCount" Text="0 Gruppen geladen" FontSize="10" Foreground="Gray"/>
</StackPanel>
<DataGrid Name="dgGroups" AutoGenerateColumns="False" CanUserAddRows="False"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="✓" Width="30">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Style="{StaticResource CenteredCheckBox}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Abteilung" Binding="{Binding DisplayName}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Einzelbenutzer">
<DockPanel Margin="5">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock Text="Benutzersuche" FontWeight="Bold" FontSize="14" Margin="0,0,0,5"/>
<Grid Margin="0,0,0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Name="txtUserSearch" Height="25" Grid.Column="0"
Tag="Name oder E-Mail eingeben..." Margin="0,0,5,0"/>
<Button Name="btnClearUserSearch" Content="✕" Width="25" Height="25"
Grid.Column="1" ToolTip="Suche zurücksetzen" Margin="0,0,5,0"/>
<Button Name="btnSearchUsers" Content="Suchen" Width="80" Height="25" Grid.Column="2"/>
</Grid>
<TextBlock Text="Abteilungssuche" FontWeight="Bold" FontSize="12" Margin="0,10,0,5"/>
<Grid Margin="0,0,0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Name="txtDeptSearch" Height="25" Grid.Column="0"
Tag="Abteilung suchen (z.B. IT, Vertrieb)..." Margin="0,0,5,0"/>
<Button Name="btnClearDeptSearch" Content="✕" Width="25" Height="25"
Grid.Column="1" ToolTip="Suche zurücksetzen" Margin="0,0,5,0"/>
<Button Name="btnSearchDept" Content="Suchen" Width="80" Height="25" Grid.Column="2"/>
</Grid>
<TextBlock Text="Oder aus Liste wählen:" FontSize="11" Margin="0,5,0,3"/>
<ComboBox Name="cmbDepartmentFilter" Height="25" Margin="0,0,0,5"/>
<TextBlock Name="txtUserCount" Text="0 Benutzer geladen" FontSize="10" Foreground="Gray"/>
</StackPanel>
<DataGrid Name="dgUsers" AutoGenerateColumns="False" CanUserAddRows="False"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="✓" Width="30">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Style="{StaticResource CenteredCheckBox}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Name" Binding="{Binding DisplayName}" Width="*"/>
<DataGridTextColumn Header="E-Mail" Binding="{Binding UserPrincipalName}" Width="200"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="RPA">
<DockPanel Margin="5">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock Text="RPA-Gruppen" FontWeight="Bold" FontSize="14" Margin="0,0,0,5"/>
<TextBlock Text="Wählen Sie RPA-Benutzergruppen für App-Zuweisungen" FontSize="11" Foreground="Gray" Margin="0,0,0,5"/>
<Button Name="btnLoadRpaGroups" Content="RPA-Gruppen laden" Height="25" Margin="0,0,0,5"/>
<TextBlock Name="txtRpaGroupCount" Text="0 RPA-Gruppen geladen" FontSize="10" Foreground="Gray"/>
</StackPanel>
<DataGrid Name="dgRpaGroups" AutoGenerateColumns="False" CanUserAddRows="False"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="✓" Width="30">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Style="{StaticResource CenteredCheckBox}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Gruppenname" Binding="{Binding DisplayName}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Gruppenabfrage">
<DockPanel Margin="5">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock Text="Beliebige Gruppe suchen" FontWeight="Bold" FontSize="14" Margin="0,0,0,5"/>
<TextBlock Text="Sucht über alle Gruppen im Tenant (nicht nur abt-hm-* / RPA)" FontSize="10" Foreground="Gray" Margin="0,0,0,5"/>
<Grid Margin="0,0,0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Name="txtGroupQuerySearch" Height="25" Grid.Column="0"
Tag="Gruppenname (Teilstring)..." Margin="0,0,5,0"/>
<Button Name="btnGroupQuerySearch" Content="Suchen" Width="70" Height="25" Grid.Column="1"/>
</Grid>
<CheckBox Name="chkResolveNestedGroups" Content="Verschachtelte Gruppen auflösen" Margin="0,3,0,0"/>
<TextBlock Text="💡 Doppelklick auf eine Gruppe zeigt deren Mitglieder" FontSize="10" Foreground="Gray" Margin="0,8,0,0"/>
<TextBlock Name="txtGroupQueryCount" Text="0 Gruppen gefunden" FontSize="10" Foreground="Gray" Margin="0,3,0,0"/>
</StackPanel>
<DataGrid Name="dgGroupQueryResults" AutoGenerateColumns="False" CanUserAddRows="False"
SelectionMode="Single" SelectionUnit="FullRow"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Gruppenname" Binding="{Binding DisplayName}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
</TabControl>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="LightGray"/>
<DockPanel Grid.Column="2" Margin="10">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock Text="Verfügbare Apps" FontWeight="Bold" FontSize="14" Margin="0,0,0,5"/>
<Grid Margin="0,0,0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Name="txtAppSearch" Height="25" Grid.Column="0"
Tag="App-Name suchen..." Margin="0,0,5,0"/>
<Button Name="btnClearAppSearch" Content="✕" Width="25" Height="25"
Grid.Column="1" ToolTip="Suche zurücksetzen" Margin="0,0,5,0"/>
</Grid>
<!-- Buttons nebeneinander -->
<Grid Margin="0,0,0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Name="btnLoadApps" Content="Apps laden" Height="25" Grid.Column="0" Margin="0,0,3,0"/>
<Button Name="btnCheckMembership" Content="🔍 Prüfen" Height="25" Grid.Column="1" Margin="3,0,0,0"
ToolTip="Prüft ob ausgewählte Gruppen/Benutzer bereits Mitglied der App-Zuweisungsgruppen sind"/>
</Grid>
<!-- Moderne Filter-Ansicht -->
<Border Name="filterPanel" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="3" Padding="5" Margin="0,0,0,5" Background="#FAFAFA">
<StackPanel>
<TextBlock Text="Filter:" FontSize="9" FontWeight="SemiBold" Margin="0,0,0,3"/>
<WrapPanel Name="wrapFilterChips">
<ComboBox Name="cmbAppFilter" Height="22" Width="180" FontSize="10" Margin="0,0,5,3"/>
<Button Name="btnClearFilter" Content="✕ Filter zurücksetzen" Height="22" FontSize="9"
Padding="5,0" Margin="0,0,0,3" Visibility="Collapsed"
Background="#FFEBEE" Foreground="#C62828" BorderBrush="#FFCDD2"/>
</WrapPanel>
<TextBlock Name="txtActiveFilter" Text="" FontSize="9" Foreground="#1565C0"
FontStyle="Italic" Visibility="Collapsed" Margin="0,2,0,0"/>
</StackPanel>
</Border>
<TextBlock Name="txtAppCount" Text="0 Apps geladen" FontSize="10" Foreground="Gray"/>
</StackPanel>
<DataGrid Name="dgApps" AutoGenerateColumns="False" CanUserAddRows="False"
SelectionMode="Single" CanUserSortColumns="True"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True">
<DataGrid.Columns>
<DataGridTextColumn Header="App Name" Binding="{Binding AppName}" Width="200" IsReadOnly="True"/>
<DataGridTemplateColumn Header="Avail." Width="50">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding UseAvailable, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Style="{StaticResource CenteredCheckBox}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Available Gruppe" Width="180">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border ToolTip="{Binding AvailableMembershipTooltip}" Padding="2">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AvailableMembershipStatus}" Value="Full">
<Setter Property="Background" Value="#C8E6C9"/>
</DataTrigger>
<DataTrigger Binding="{Binding AvailableMembershipStatus}" Value="Partial">
<Setter Property="Background" Value="#FFF9C4"/>
</DataTrigger>
<DataTrigger Binding="{Binding AvailableMembershipStatus}" Value="Native">
<Setter Property="Background" Value="#E3F2FD"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<ComboBox ItemsSource="{Binding AvailableGroups}"
SelectedItem="{Binding SelectedAvailableGroup, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding UseAvailable}">
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding AvailableGroupsCount}" Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding AvailableGroupsCount}" Value="1">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<TextBlock Text="{Binding AvailableGroupDisplayText}" VerticalAlignment="Center" Padding="3">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AvailableGroupsCount}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
<Setter Property="Foreground" Value="Gray"/>
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
<DataTrigger Binding="{Binding AvailableGroupsCount}" Value="1">
<Setter Property="Visibility" Value="Visible"/>
<Setter Property="Foreground" Value="Black"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Req." Width="50">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding UseRequired, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Style="{StaticResource CenteredCheckBox}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Required Gruppe" Width="180">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border ToolTip="{Binding RequiredMembershipTooltip}" Padding="2">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RequiredMembershipStatus}" Value="Full">
<Setter Property="Background" Value="#C8E6C9"/>
</DataTrigger>
<DataTrigger Binding="{Binding RequiredMembershipStatus}" Value="Partial">
<Setter Property="Background" Value="#FFF9C4"/>
</DataTrigger>
<DataTrigger Binding="{Binding RequiredMembershipStatus}" Value="Native">
<Setter Property="Background" Value="#E3F2FD"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<ComboBox ItemsSource="{Binding RequiredGroups}"
SelectedItem="{Binding SelectedRequiredGroup, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding UseRequired}">
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding RequiredGroupsCount}" Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding RequiredGroupsCount}" Value="1">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<TextBlock Text="{Binding RequiredGroupDisplayText}" VerticalAlignment="Center" Padding="3">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RequiredGroupsCount}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
<Setter Property="Foreground" Value="Gray"/>
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
<DataTrigger Binding="{Binding RequiredGroupsCount}" Value="1">
<Setter Property="Visibility" Value="Visible"/>
<Setter Property="Foreground" Value="Black"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Button Name="btnCreateRequiredGroup" Content="+" Grid.Column="1"
Width="25" Height="20" Margin="2,0,0,0"
ToolTip="Required-Gruppe erstellen"
Tag="{Binding}">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RequiredGroupsCount}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Status" Binding="{Binding StatusInfo}" Width="80" IsReadOnly="True">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="ToolTip" Value="{Binding StatusDetails}"/>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
<GridSplitter Grid.Column="3" Width="5" HorizontalAlignment="Stretch" Background="LightGray"/>
<!-- Session-Übersicht Panel (horizontal geteilt) -->
<Grid Grid.Column="4" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="5"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Obere Hälfte: Session-Zuweisungen -->
<DockPanel Grid.Row="0">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,5">
<TextBlock Text="Session-Zuweisungen" FontWeight="Bold" FontSize="13" Margin="0,0,0,3"/>
<TextBlock Name="txtSessionCount" Text="0 Zuweisungen in dieser Session" FontSize="9" Foreground="Gray" Margin="0,0,0,3"/>
<Button Name="btnClearSession" Content="Session löschen" Height="22" FontSize="10"
Background="#ffcccc" ToolTip="Entfernt alle Zuweisungen aus der aktuellen Session"/>
</StackPanel>
<Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="3">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="pnlSessionAssignments" Margin="5"/>
</ScrollViewer>
</Border>
</DockPanel>
<GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch" Background="LightGray"/>
<!-- Untere Hälfte: Bestehende App-Zuweisungen -->
<DockPanel Grid.Row="2">
<StackPanel DockPanel.Dock="Top" Margin="0,5,0,5">
<TextBlock Text="Bestehende Zuweisungen" FontWeight="Bold" FontSize="13" Margin="0,0,0,3"/>
<TextBlock Name="txtExistingAssignmentsInfo" Text="Wählen Sie Gruppen/Benutzer und klicken Sie 'Mitgliedschaft prüfen'"
FontSize="9" Foreground="Gray" TextWrapping="Wrap" Margin="0,0,0,3"/>
</StackPanel>
<Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="3">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Sticky Header - Margin rechts für Scrollbar-Breite (17px) -->
<Grid Name="gridExistingHeader" Grid.Row="0" Background="#F5F5F5" Margin="5,5,22,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="App" FontWeight="SemiBold" FontSize="10" Grid.Column="0" VerticalAlignment="Center"/>
<TextBlock Text="Avail." FontWeight="SemiBold" FontSize="10" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="Req." FontWeight="SemiBold" FontSize="10" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<Border Grid.Row="0" BorderBrush="LightGray" BorderThickness="0,0,0,1" Margin="5,0" VerticalAlignment="Bottom"/>
<!-- Scrollable Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Name="pnlExistingAssignments" Margin="5"/>
</ScrollViewer>
</Grid>
</Border>
</DockPanel>
</Grid>
</Grid>
<Border Grid.Row="2" Background="#F0F0F0" Padding="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Vertical" Grid.Column="0">
<TextBlock Name="txtSummary" Text="Keine Auswahl" FontWeight="Bold" Margin="0,0,0,5"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ProgressBar Name="pbProgress" Height="20" Visibility="Collapsed" Margin="0,0,10,5" Grid.Column="0"/>
<TextBlock Name="txtProgress" Text="" VerticalAlignment="Center" Grid.Column="1"/>
</Grid>
</StackPanel>
<!-- Legende im Footer -->
<StackPanel Orientation="Horizontal" Grid.Column="1" VerticalAlignment="Center" Margin="0,0,20,0">
<TextBlock Text="Legende:" FontSize="11" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,10,0"/>
<Border Background="#C8E6C9" Width="16" Height="16" CornerRadius="2" Margin="0,0,4,0" BorderBrush="#4CAF50" BorderThickness="1"/>
<TextBlock Text="Mitglied" FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,12,0"/>
<Border Background="#FFF9C4" Width="16" Height="16" CornerRadius="2" Margin="0,0,4,0" BorderBrush="#FBC02D" BorderThickness="1"/>
<TextBlock Text="Teilweise" FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,12,0"/>
<Border Background="#E3F2FD" Width="16" Height="16" CornerRadius="2" Margin="0,0,4,0" BorderBrush="#2196F3" BorderThickness="1"/>
<TextBlock Text="Native" FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,0,0"/>
</StackPanel>
<Button Name="btnApply" Content="Zuweisungen anwenden" Width="180" Height="35"
Grid.Column="2" IsEnabled="False"
Background="#0da075" Foreground="White" FontWeight="Bold"/>
</Grid>
</Border>
</Grid>
</Window>
"@
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
$btnConnect = $window.FindName("btnConnect")
$txtStatus = $window.FindName("txtStatus")
$tabMain = $window.FindName("tabMain")
$btnLoadGroups = $window.FindName("btnLoadGroups")
$btnLoadApps = $window.FindName("btnLoadApps")
$btnCheckMembership = $window.FindName("btnCheckMembership")
$dgGroups = $window.FindName("dgGroups")
$dgUsers = $window.FindName("dgUsers")
$dgRpaGroups = $window.FindName("dgRpaGroups")
$btnLoadRpaGroups = $window.FindName("btnLoadRpaGroups")
$txtRpaGroupCount = $window.FindName("txtRpaGroupCount")
$dgApps = $window.FindName("dgApps")
$txtGroupCount = $window.FindName("txtGroupCount")
$txtUserCount = $window.FindName("txtUserCount")
$txtAppCount = $window.FindName("txtAppCount")
$btnApply = $window.FindName("btnApply")
$txtSummary = $window.FindName("txtSummary")
$pbProgress = $window.FindName("pbProgress")
$txtProgress = $window.FindName("txtProgress")
$txtUserSearch = $window.FindName("txtUserSearch")
$btnSearchUsers = $window.FindName("btnSearchUsers")
$btnClearUserSearch = $window.FindName("btnClearUserSearch")
$txtDeptSearch = $window.FindName("txtDeptSearch")
$btnSearchDept = $window.FindName("btnSearchDept")
$btnClearDeptSearch = $window.FindName("btnClearDeptSearch")
$cmbDepartmentFilter = $window.FindName("cmbDepartmentFilter")
$txtGroupSearch = $window.FindName("txtGroupSearch")
$btnClearGroupSearch = $window.FindName("btnClearGroupSearch")
$txtAppSearch = $window.FindName("txtAppSearch")
$btnClearAppSearch = $window.FindName("btnClearAppSearch")
$cmbAppFilter = $window.FindName("cmbAppFilter")
$btnClearFilter = $window.FindName("btnClearFilter")
$txtActiveFilter = $window.FindName("txtActiveFilter")
$txtSessionCount = $window.FindName("txtSessionCount")
$btnClearSession = $window.FindName("btnClearSession")
$pnlSessionAssignments = $window.FindName("pnlSessionAssignments")
$txtExistingAssignmentsInfo = $window.FindName("txtExistingAssignmentsInfo")
$pnlExistingAssignments = $window.FindName("pnlExistingAssignments")
$txtGroupQuerySearch = $window.FindName("txtGroupQuerySearch")
$btnGroupQuerySearch = $window.FindName("btnGroupQuerySearch")
$chkResolveNestedGroups = $window.FindName("chkResolveNestedGroups")
$txtGroupQueryCount = $window.FindName("txtGroupQueryCount")
$dgGroupQueryResults = $window.FindName("dgGroupQueryResults")
$script:connected = $false
$script:groups = @()
$script:allGroups = @()
$script:users = @()
$script:apps = @()
$script:allApps = @()
$script:currentFilter = "Alle Apps"
# Session-Zuweisungen: Speichert alle Zuweisungen die in dieser Session gemacht wurden
# Format: @{ TargetId_AppId_Type = @{ TargetId; TargetName; TargetType; AppId; AppName; AssignmentType; GroupId; GroupName } }
$script:sessionAssignments = @{}
# Separate App-Auswahl für Gruppen-Modus, Benutzer-Modus und RPA-Modus
# Format: @{ AppId = @{ UseAvailable = $true/$false; UseRequired = $true/$false } }
$script:groupModeAppSelections = @{}
$script:userModeAppSelections = @{}
$script:rpaModeAppSelections = @{}
$script:lastTabIndex = 0 # Speichert den letzten Tab-Index für Wechsel-Erkennung
# RPA-Gruppen
$script:rpaGroups = @()
# Gruppenabfrage (freie Gruppensuche + Mitgliederanzeige)
$script:groupQueryResults = @()
try {
Add-Type @"
using System.Collections.ObjectModel;
public class IntuneGroupItem {
public bool IsSelected { get; set; }
public string Id { get; set; }
public string DisplayName { get; set; }
}
public class IntuneUserItem {
public bool IsSelected { get; set; }
public string Id { get; set; }
public string DisplayName { get; set; }
public string UserPrincipalName { get; set; }
}
public class IntuneGroupAssignment {
public string GroupId { get; set; }
public string DisplayName { get; set; }
public override string ToString() {
return DisplayName;
}
}
public class IntuneAppItem {
public string AppName { get; set; }
public string AppId { get; set; }
public bool UseAvailable { get; set; }
public ObservableCollection<IntuneGroupAssignment> AvailableGroups { get; set; }
public IntuneGroupAssignment SelectedAvailableGroup { get; set; }
public int AvailableGroupsCount { get; set; }
public string AvailableGroupDisplayText { get; set; }
public bool UseRequired { get; set; }
public ObservableCollection<IntuneGroupAssignment> RequiredGroups { get; set; }
public IntuneGroupAssignment SelectedRequiredGroup { get; set; }
public int RequiredGroupsCount { get; set; }
public string RequiredGroupDisplayText { get; set; }
public string StatusInfo { get; set; }
public string StatusDetails { get; set; }
// Mitgliedschafts-Status: "None", "Partial", "Full", "Native"
public string AvailableMembershipStatus { get; set; }
public string RequiredMembershipStatus { get; set; }
public string AvailableMembershipTooltip { get; set; }
public string RequiredMembershipTooltip { get; set; }
public IntuneAppItem() {
AvailableGroups = new ObservableCollection<IntuneGroupAssignment>();
RequiredGroups = new ObservableCollection<IntuneGroupAssignment>();
AvailableMembershipStatus = "None";
RequiredMembershipStatus = "None";
AvailableMembershipTooltip = "";
RequiredMembershipTooltip = "";
}
}
"@
} catch { }
function Clear-GroupName {
param([string]$Name)
$cleaned = $Name -replace '[äÄ]', 'ae' -replace '[öÖ]', 'oe' -replace '[üÜ]', 'ue' -replace '[ß]', 'ss' -replace '[^a-zA-Z0-9\-_]', '-' -replace '\-+', '-'
return $cleaned.Trim('-').ToLower()
}
# ===== SESSION-ZUWEISUNGS-FUNKTIONEN =====
function Add-SessionAssignment {
param(
[string]$TargetId,
[string]$TargetName,
[string]$TargetType, # "Group" oder "User"
[string]$AppId,
[string]$AppName,
[string]$AssignmentType, # "Available" oder "Required"
[string]$GroupId,
[string]$GroupName
)
$key = "$TargetId`_$AppId`_$AssignmentType"
$script:sessionAssignments[$key] = @{
TargetId = $TargetId
TargetName = $TargetName
TargetType = $TargetType
AppId = $AppId
AppName = $AppName
AssignmentType = $AssignmentType
GroupId = $GroupId
GroupName = $GroupName
}
Update-SessionPanel
}
function Remove-SessionAssignment {
param([string]$Key)
if ($script:sessionAssignments.ContainsKey($Key)) {
$script:sessionAssignments.Remove($Key)
Update-SessionPanel
}
}
function Clear-AllSessionAssignments {
$script:sessionAssignments = @{}
# Auch alle Checkboxen zurücksetzen
foreach ($group in $script:allGroups) { $group.IsSelected = $false }
foreach ($user in $script:users) { $user.IsSelected = $false }
foreach ($app in $script:allApps) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
$dgGroups.Items.Refresh()
$dgUsers.Items.Refresh()
$dgApps.Items.Refresh()
Update-SessionPanel
Update-Summary
}
function Update-SessionPanel {
$pnlSessionAssignments.Children.Clear()
if ($script:sessionAssignments.Count -eq 0) {
$txtSessionCount.Text = "0 Zuweisungen in dieser Session"
$emptyText = New-Object System.Windows.Controls.TextBlock
$emptyText.Text = "Keine Zuweisungen vorhanden.`n`nWählen Sie Gruppen/Benutzer und Apps aus, um Zuweisungen zu erstellen."
$emptyText.Foreground = "Gray"
$emptyText.FontStyle = "Italic"
$emptyText.TextWrapping = "Wrap"
$emptyText.Margin = "5"
$pnlSessionAssignments.Children.Add($emptyText) | Out-Null
return
}
# Gruppiere nach Target
$byTarget = @{}
foreach ($key in $script:sessionAssignments.Keys) {
$assignment = $script:sessionAssignments[$key]
$targetKey = $assignment.TargetId
if (-not $byTarget.ContainsKey($targetKey)) {
$byTarget[$targetKey] = @{
TargetName = $assignment.TargetName
TargetType = $assignment.TargetType
Assignments = @()
}
}
$byTarget[$targetKey].Assignments += @{
Key = $key
AppName = $assignment.AppName
AssignmentType = $assignment.AssignmentType
GroupName = $assignment.GroupName
}
}
$totalCount = $script:sessionAssignments.Count
$txtSessionCount.Text = "$totalCount Zuweisung(en) in dieser Session"
foreach ($targetId in $byTarget.Keys) {
$target = $byTarget[$targetId]
# Target-Header
$targetBorder = New-Object System.Windows.Controls.Border
$targetBorder.Background = if ($target.TargetType -eq "Group") { "#E8F5E9" } else { "#E3F2FD" }
$targetBorder.Padding = "8"
$targetBorder.Margin = "0,0,0,8"
$targetBorder.CornerRadius = "4"
$targetStack = New-Object System.Windows.Controls.StackPanel
# Target-Name mit Icon
$headerStack = New-Object System.Windows.Controls.StackPanel
$headerStack.Orientation = "Horizontal"
$icon = New-Object System.Windows.Controls.TextBlock
$icon.Text = if ($target.TargetType -eq "Group") { "👥 " } else { "👤 " }
$icon.FontSize = 12
$headerStack.Children.Add($icon) | Out-Null
$nameText = New-Object System.Windows.Controls.TextBlock
$nameText.Text = $target.TargetName
$nameText.FontWeight = "SemiBold"
$nameText.FontSize = 11
$nameText.TextTrimming = "CharacterEllipsis"
$headerStack.Children.Add($nameText) | Out-Null
$targetStack.Children.Add($headerStack) | Out-Null
# Zuweisungen für dieses Target
foreach ($assign in $target.Assignments) {
$assignGrid = New-Object System.Windows.Controls.Grid
$assignGrid.Margin = "0,4,0,0"
$col1 = New-Object System.Windows.Controls.ColumnDefinition
$col1.Width = "*"
$col2 = New-Object System.Windows.Controls.ColumnDefinition
$col2.Width = "Auto"
$col3 = New-Object System.Windows.Controls.ColumnDefinition
$col3.Width = "Auto"
$assignGrid.ColumnDefinitions.Add($col1) | Out-Null
$assignGrid.ColumnDefinitions.Add($col2) | Out-Null
$assignGrid.ColumnDefinitions.Add($col3) | Out-Null
$appText = New-Object System.Windows.Controls.TextBlock
$appText.Text = $assign.AppName
$appText.FontSize = 10
$appText.TextTrimming = "CharacterEllipsis"
$appText.ToolTip = "$($assign.AppName) -> $($assign.GroupName)"
$appText.VerticalAlignment = "Center"
[System.Windows.Controls.Grid]::SetColumn($appText, 0)
$assignGrid.Children.Add($appText) | Out-Null
$typeLabel = New-Object System.Windows.Controls.TextBlock
$typeLabel.Text = if ($assign.AssignmentType -eq "Available") { "A" } else { "R" }
$typeLabel.FontSize = 9
$typeLabel.FontWeight = "Bold"
$typeLabel.Padding = "4,1"
$typeLabel.Margin = "4,0"
$typeLabel.VerticalAlignment = "Center"
if ($assign.AssignmentType -eq "Available") {
$typeLabel.Foreground = "#2E7D32"
} else {
$typeLabel.Foreground = "#C62828"
}
$typeLabel.ToolTip = $assign.AssignmentType
[System.Windows.Controls.Grid]::SetColumn($typeLabel, 1)
$assignGrid.Children.Add($typeLabel) | Out-Null
# Löschen-Button
$removeBtn = New-Object System.Windows.Controls.Button
$removeBtn.Content = "✕"
$removeBtn.Width = 18
$removeBtn.Height = 18
$removeBtn.FontSize = 9
$removeBtn.Padding = "0"
$removeBtn.ToolTip = "Diese Zuweisung entfernen"
$removeBtn.Tag = $assign.Key
$removeBtn.Add_Click({
param($sender, $e)
$keyToRemove = $sender.Tag
Remove-SessionAssignment -Key $keyToRemove
})
[System.Windows.Controls.Grid]::SetColumn($removeBtn, 2)
$assignGrid.Children.Add($removeBtn) | Out-Null
$targetStack.Children.Add($assignGrid) | Out-Null
}
$targetBorder.Child = $targetStack
$pnlSessionAssignments.Children.Add($targetBorder) | Out-Null
}
}
function Save-CurrentSelectionsToSession {
# Diese Funktion speichert alle aktuell ausgewählten Kombinationen in die Session
$selectedTargets = @()
$targetType = ""
switch ($tabMain.SelectedIndex) {
0 {
# Gruppen-Tab
if ($dgGroups.ItemsSource) {
foreach ($item in $dgGroups.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$selectedTargets += @{ Id = $item.Id; Name = $item.DisplayName; Type = "Group" }
}
}
}
$targetType = "Group"
}
1 {
# Benutzer-Tab
if ($dgUsers.ItemsSource) {
foreach ($item in $dgUsers.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$selectedTargets += @{ Id = $item.Id; Name = $item.DisplayName; Type = "User" }
}
}
}
$targetType = "User"
}
2 {
# RPA-Tab (RPA-Gruppen sind auch Gruppen)
if ($dgRpaGroups.ItemsSource) {
foreach ($item in $dgRpaGroups.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$selectedTargets += @{ Id = $item.Id; Name = $item.DisplayName; Type = "Group" }
}
}
}
$targetType = "Group"
}
}
# Für jedes ausgewählte Target und jede ausgewählte App: Session-Zuweisung erstellen
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
foreach ($target in $selectedTargets) {
if ($app.UseAvailable -eq $true -and $null -ne $app.SelectedAvailableGroup -and $app.SelectedAvailableGroup.GroupId -ne "") {
Add-SessionAssignment -TargetId $target.Id -TargetName $target.Name -TargetType $target.Type `
-AppId $app.AppId -AppName $app.AppName -AssignmentType "Available" `
-GroupId $app.SelectedAvailableGroup.GroupId -GroupName $app.SelectedAvailableGroup.DisplayName
}
if ($app.UseRequired -eq $true -and $null -ne $app.SelectedRequiredGroup -and $app.SelectedRequiredGroup.GroupId -ne "") {
Add-SessionAssignment -TargetId $target.Id -TargetName $target.Name -TargetType $target.Type `
-AppId $app.AppId -AppName $app.AppName -AssignmentType "Required" `
-GroupId $app.SelectedRequiredGroup.GroupId -GroupName $app.SelectedRequiredGroup.DisplayName
}
}
}
}
}
}
function Restore-SelectionsFromSession {
# Diese Funktion stellt Auswahlen aus der Session wieder her für die aktuell geladenen Items
# Gruppen wiederherstellen
if ($script:allGroups.Count -gt 0) {
foreach ($group in $script:allGroups) {
$hasAssignment = $false
foreach ($key in $script:sessionAssignments.Keys) {
$assignment = $script:sessionAssignments[$key]
if ($assignment.TargetId -eq $group.Id -and $assignment.TargetType -eq "Group") {
$hasAssignment = $true
break
}
}
$group.IsSelected = $hasAssignment
}
$dgGroups.Items.Refresh()
}
# Benutzer wiederherstellen
if ($script:users.Count -gt 0) {
foreach ($user in $script:users) {
$hasAssignment = $false
foreach ($key in $script:sessionAssignments.Keys) {
$assignment = $script:sessionAssignments[$key]
if ($assignment.TargetId -eq $user.Id -and $assignment.TargetType -eq "User") {
$hasAssignment = $true
break
}
}
$user.IsSelected = $hasAssignment
}
$dgUsers.Items.Refresh()
}
# RPA-Gruppen wiederherstellen
if ($script:rpaGroups.Count -gt 0) {
foreach ($rpaGroup in $script:rpaGroups) {
$hasAssignment = $false
foreach ($key in $script:sessionAssignments.Keys) {
$assignment = $script:sessionAssignments[$key]
if ($assignment.TargetId -eq $rpaGroup.Id -and $assignment.TargetType -eq "Group") {
$hasAssignment = $true
break
}
}
$rpaGroup.IsSelected = $hasAssignment
}
$dgRpaGroups.Items.Refresh()
}
# App-Checkboxen können nicht einfach wiederhergestellt werden, da sie target-spezifisch sind
# Sie werden nur zurückgesetzt wenn keine aktiven Zuweisungen mehr vorhanden sind
}
function Invoke-SessionAssignments {
# Diese Funktion führt alle Session-Zuweisungen aus
$pbProgress.Visibility = "Visible"
$btnApply.IsEnabled = $false
$totalOperations = $script:sessionAssignments.Count
$currentOperation = 0
$successCount = 0
$errorCount = 0
$skippedCount = 0
# Sammle Daten für den Report
$reportTargets = @()
$reportAssignments = @()
$hasUsers = $false
foreach ($key in $script:sessionAssignments.Keys) {
$assignment = $script:sessionAssignments[$key]
$currentOperation++
$pbProgress.Value = ($currentOperation / $totalOperations) * 100
$txtProgress.Text = "$($assignment.TargetName) -> $($assignment.AppName) ($($assignment.GroupName) - $($assignment.AssignmentType))"
[System.Windows.Forms.Application]::DoEvents()
# Report-Daten sammeln
if (-not ($reportTargets | Where-Object { $_.Id -eq $assignment.TargetId })) {
$reportTargets += [PSCustomObject]@{ Id = $assignment.TargetId; DisplayName = $assignment.TargetName }
}
if (-not ($reportAssignments | Where-Object { $_.AppName -eq $assignment.AppName -and $_.GroupId -eq $assignment.GroupId -and $_.Type -eq $assignment.AssignmentType })) {
$reportAssignments += @{
AppName = $assignment.AppName
GroupId = $assignment.GroupId
GroupName = $assignment.GroupName
Type = $assignment.AssignmentType
}
}
if ($assignment.TargetType -eq "User") { $hasUsers = $true }
# Native Zuweisungen überspringen
if ($assignment.GroupId -eq "ALL_USERS" -or $assignment.GroupId -eq "ALL_DEVICES") {
$skippedCount++
continue
}
try {
Add-GraphGroupMember -GroupId $assignment.GroupId -DirectoryObjectId $assignment.TargetId
$successCount++
}
catch {
if ($_.Exception.Message -like "*already exist*" -or $_.Exception.Message -like "*bereits vorhanden*" -or $_.Exception.Message -like "*One or more added object references already exist*") {
$successCount++
}
else {
$errorCount++
}
}
}
$pbProgress.Visibility = "Collapsed"
$btnApply.IsEnabled = $true
$txtProgress.Text = ""
# HTML-Report erstellen
$reportPath = New-AssignmentReport -Targets $reportTargets -Assignments $reportAssignments -IsUserMode $hasUsers `
-SuccessCount $successCount -ErrorCount $errorCount -SkippedCount $skippedCount
$resultMessage = "Zuweisungen abgeschlossen:`n`nErfolgreich: $successCount`n"
if ($skippedCount -gt 0) { $resultMessage += "Übersprungen (Native): $skippedCount`n" }
$resultMessage += "Fehler: $errorCount`nGesamt: $totalOperations"
$resultMessage += "`n`nReport erstellt: $reportPath`n`nReport jetzt öffnen?"
$openReport = [System.Windows.Forms.MessageBox]::Show($resultMessage, "Abgeschlossen", "YesNo", "Information")
if ($openReport -eq "Yes") {
Start-Process $reportPath
}
# Session und UI zurücksetzen
Clear-AllSessionAssignments
# App-Auswahl für beide Modi zurücksetzen
$script:groupModeAppSelections = @{}
$script:userModeAppSelections = @{}
}
function Save-AppSelectionsForCurrentMode {
# Speichert die aktuelle App-Auswahl für den aktuellen Modus (Gruppen oder Benutzer)
$currentSelections = @{}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app -and ($app.UseAvailable -eq $true -or $app.UseRequired -eq $true)) {
$currentSelections[$app.AppId] = @{
UseAvailable = $app.UseAvailable
UseRequired = $app.UseRequired
SelectedAvailableGroup = $app.SelectedAvailableGroup
SelectedRequiredGroup = $app.SelectedRequiredGroup
}
}
}
}
switch ($script:lastTabIndex) {
0 { $script:groupModeAppSelections = $currentSelections }
1 { $script:userModeAppSelections = $currentSelections }
2 { $script:rpaModeAppSelections = $currentSelections }
}
}
function Restore-AppSelectionsForMode {
param([int]$TabIndex)
# Stellt die App-Auswahl für den angegebenen Modus wieder her
$selections = switch ($TabIndex) {
0 { $script:groupModeAppSelections }
1 { $script:userModeAppSelections }
2 { $script:rpaModeAppSelections }
default { @{} }
}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
if ($selections.ContainsKey($app.AppId)) {
$saved = $selections[$app.AppId]
$app.UseAvailable = $saved.UseAvailable
$app.UseRequired = $saved.UseRequired
if ($null -ne $saved.SelectedAvailableGroup) {
$app.SelectedAvailableGroup = $saved.SelectedAvailableGroup
}
if ($null -ne $saved.SelectedRequiredGroup) {
$app.SelectedRequiredGroup = $saved.SelectedRequiredGroup
}
}
else {
# Keine gespeicherte Auswahl -> zurücksetzen
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
}
$dgApps.Items.Refresh()
}
}
function Clear-CurrentModeAppSelections {
# Setzt die App-Checkboxen für den aktuellen Modus zurück (nach dem Speichern in Session)
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
$dgApps.Items.Refresh()
}
# Auch die gespeicherte Auswahl für den aktuellen Modus löschen
switch ($tabMain.SelectedIndex) {
0 { $script:groupModeAppSelections = @{} }
1 { $script:userModeAppSelections = @{} }
2 { $script:rpaModeAppSelections = @{} }
}
}
# Cache für Gruppenmitgliedschaften (um wiederholte API-Aufrufe zu vermeiden)
$script:membershipCache = @{}
function Update-MembershipIndicators {
# Prüft für alle ausgewählten Targets, ob sie bereits Mitglied der App-Zuweisungsgruppen sind
# Sammle ausgewählte Targets
$selectedTargets = @()
switch ($tabMain.SelectedIndex) {
0 {
# Gruppen-Tab
if ($dgGroups.ItemsSource) {
foreach ($item in $dgGroups.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$selectedTargets += @{ Id = $item.Id; Name = $item.DisplayName; Type = "Group" }
}
}
}
}
1 {
# Benutzer-Tab
if ($dgUsers.ItemsSource) {
foreach ($item in $dgUsers.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$selectedTargets += @{ Id = $item.Id; Name = $item.DisplayName; Type = "User" }
}
}
}
}
2 {
# RPA-Tab (RPA-Gruppen sind auch Gruppen)
if ($dgRpaGroups.ItemsSource) {
foreach ($item in $dgRpaGroups.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$selectedTargets += @{ Id = $item.Id; Name = $item.DisplayName; Type = "Group" }
}
}
}
}
}
# Wenn keine Targets ausgewählt, alle Status zurücksetzen
if ($selectedTargets.Count -eq 0) {
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.AvailableMembershipStatus = "None"
$app.RequiredMembershipStatus = "None"
$app.AvailableMembershipTooltip = ""
$app.RequiredMembershipTooltip = ""
}
}
$dgApps.Items.Refresh()
}
return
}
# Lade Gruppenmitgliedschaften für alle Targets (mit Cache)
$targetMemberships = @{}
foreach ($target in $selectedTargets) {
if (-not $script:membershipCache.ContainsKey($target.Id)) {
try {
$memberships = @()
if ($target.Type -eq "User") {
# Benutzer: Direkte Gruppenmitgliedschaften laden
$groups = Get-MgUserMemberOf -UserId $target.Id -All -Property Id 2>$null
foreach ($g in $groups) {
if ($g.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.group') {
$memberships += $g.Id
}
}
}
else {
# Gruppe: Transitive Mitgliedschaften laden (Gruppe ist Mitglied von...)
$groups = Get-MgGroupMemberOf -GroupId $target.Id -All -Property Id 2>$null
foreach ($g in $groups) {
if ($g.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.group') {
$memberships += $g.Id
}
}
}
$script:membershipCache[$target.Id] = $memberships
}
catch {
$script:membershipCache[$target.Id] = @()
}
}
$targetMemberships[$target.Id] = $script:membershipCache[$target.Id]
}
# Für jede App den Mitgliedschaftsstatus prüfen
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -eq $app) { continue }
# Available-Gruppe prüfen
$availableGroupId = $null
if ($null -ne $app.SelectedAvailableGroup) {
$availableGroupId = $app.SelectedAvailableGroup.GroupId
}
if ([string]::IsNullOrEmpty($availableGroupId)) {
$app.AvailableMembershipStatus = "None"
$app.AvailableMembershipTooltip = ""
}
elseif ($availableGroupId -eq "ALL_USERS" -or $availableGroupId -eq "ALL_DEVICES") {
$app.AvailableMembershipStatus = "Native"
$app.AvailableMembershipTooltip = "Native Zuweisung (All Users/All Devices)"
}
else {
$membersInGroup = @()
$membersNotInGroup = @()
foreach ($target in $selectedTargets) {
if ($targetMemberships[$target.Id] -contains $availableGroupId) {
$membersInGroup += $target.Name
}
else {
$membersNotInGroup += $target.Name
}
}
if ($membersNotInGroup.Count -eq 0) {
$app.AvailableMembershipStatus = "Full"
$app.AvailableMembershipTooltip = "Bereits Mitglied: " + ($membersInGroup -join ", ")
}
elseif ($membersInGroup.Count -gt 0) {
$app.AvailableMembershipStatus = "Partial"
$app.AvailableMembershipTooltip = "Mitglied: " + ($membersInGroup -join ", ") + "`nNicht Mitglied: " + ($membersNotInGroup -join ", ")
}
else {
$app.AvailableMembershipStatus = "None"
$app.AvailableMembershipTooltip = "Nicht Mitglied: " + ($membersNotInGroup -join ", ")
}
}
# Required-Gruppe prüfen
$requiredGroupId = $null
if ($null -ne $app.SelectedRequiredGroup) {
$requiredGroupId = $app.SelectedRequiredGroup.GroupId
}
if ([string]::IsNullOrEmpty($requiredGroupId)) {
$app.RequiredMembershipStatus = "None"
$app.RequiredMembershipTooltip = ""
}
elseif ($requiredGroupId -eq "ALL_USERS" -or $requiredGroupId -eq "ALL_DEVICES") {
$app.RequiredMembershipStatus = "Native"
$app.RequiredMembershipTooltip = "Native Zuweisung (All Users/All Devices)"
}
else {
$membersInGroup = @()
$membersNotInGroup = @()
foreach ($target in $selectedTargets) {
if ($targetMemberships[$target.Id] -contains $requiredGroupId) {
$membersInGroup += $target.Name
}
else {
$membersNotInGroup += $target.Name
}
}
if ($membersNotInGroup.Count -eq 0) {
$app.RequiredMembershipStatus = "Full"
$app.RequiredMembershipTooltip = "Bereits Mitglied: " + ($membersInGroup -join ", ")
}
elseif ($membersInGroup.Count -gt 0) {
$app.RequiredMembershipStatus = "Partial"
$app.RequiredMembershipTooltip = "Mitglied: " + ($membersInGroup -join ", ") + "`nNicht Mitglied: " + ($membersNotInGroup -join ", ")
}
else {
$app.RequiredMembershipStatus = "None"
$app.RequiredMembershipTooltip = "Nicht Mitglied: " + ($membersNotInGroup -join ", ")
}
}
}
$dgApps.Items.Refresh()
}
}
function Clear-MembershipCache {
$script:membershipCache = @{}
}
function Update-ExistingAssignmentsPanel {
# Zeigt die bestehenden App-Zuweisungen für die ausgewählten Targets an
$pnlExistingAssignments.Children.Clear()
# Sammle Apps mit Mitgliedschaft (Full oder Partial)
$existingAssignments = @()
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -eq $app) { continue }
$hasAvailable = $app.AvailableMembershipStatus -eq "Full" -or $app.AvailableMembershipStatus -eq "Partial" -or $app.AvailableMembershipStatus -eq "Native"
$hasRequired = $app.RequiredMembershipStatus -eq "Full" -or $app.RequiredMembershipStatus -eq "Partial" -or $app.RequiredMembershipStatus -eq "Native"
if ($hasAvailable -or $hasRequired) {
$existingAssignments += @{
AppName = $app.AppName
AvailableStatus = $app.AvailableMembershipStatus
AvailableTooltip = $app.AvailableMembershipTooltip
AvailableGroup = if ($null -ne $app.SelectedAvailableGroup) { $app.SelectedAvailableGroup.DisplayName } else { "" }
RequiredStatus = $app.RequiredMembershipStatus
RequiredTooltip = $app.RequiredMembershipTooltip
RequiredGroup = if ($null -ne $app.SelectedRequiredGroup) { $app.SelectedRequiredGroup.DisplayName } else { "" }
}
}
}
}
if ($existingAssignments.Count -eq 0) {
$txtExistingAssignmentsInfo.Text = "Keine bestehenden Zuweisungen für die Auswahl gefunden"
$emptyText = New-Object System.Windows.Controls.TextBlock
$emptyText.Text = "Die ausgewählten Gruppen/Benutzer sind noch keinen App-Zuweisungsgruppen zugeordnet."
$emptyText.Foreground = "Gray"
$emptyText.FontStyle = "Italic"
$emptyText.TextWrapping = "Wrap"
$emptyText.FontSize = 10
$emptyText.Margin = "5"
$pnlExistingAssignments.Children.Add($emptyText) | Out-Null
return
}
$txtExistingAssignmentsInfo.Text = "$($existingAssignments.Count) App(s) mit bestehenden Zuweisungen"
# App-Zeilen (Header ist jetzt in XAML als sticky header)
$rowIndex = 0
foreach ($assign in $existingAssignments) {
$rowGrid = New-Object System.Windows.Controls.Grid
$rowGrid.Margin = "0,2,0,2"
$rowCol1 = New-Object System.Windows.Controls.ColumnDefinition
$rowCol1.Width = "*"
$rowCol2 = New-Object System.Windows.Controls.ColumnDefinition
$rowCol2.Width = "50"
$rowCol3 = New-Object System.Windows.Controls.ColumnDefinition
$rowCol3.Width = "50"
$rowGrid.ColumnDefinitions.Add($rowCol1) | Out-Null
$rowGrid.ColumnDefinitions.Add($rowCol2) | Out-Null
$rowGrid.ColumnDefinitions.Add($rowCol3) | Out-Null
# App Name
$appText = New-Object System.Windows.Controls.TextBlock
$appText.Text = $assign.AppName
$appText.FontSize = 10
$appText.TextTrimming = "CharacterEllipsis"
$appText.VerticalAlignment = "Center"
$appText.ToolTip = $assign.AppName
[System.Windows.Controls.Grid]::SetColumn($appText, 0)
$rowGrid.Children.Add($appText) | Out-Null
# Available Indikator
$availBorder = New-Object System.Windows.Controls.Border
$availBorder.Width = 20
$availBorder.Height = 16
$availBorder.CornerRadius = "3"
$availBorder.HorizontalAlignment = "Center"
if ($assign.AvailableStatus -eq "Full") {
$availBorder.Background = "#C8E6C9"
$availBorder.ToolTip = "Bereits Mitglied: $($assign.AvailableGroup)`n$($assign.AvailableTooltip)"
}
elseif ($assign.AvailableStatus -eq "Partial") {
$availBorder.Background = "#FFF9C4"
$availBorder.ToolTip = "Teilweise Mitglied: $($assign.AvailableGroup)`n$($assign.AvailableTooltip)"
}
elseif ($assign.AvailableStatus -eq "Native") {
$availBorder.Background = "#E3F2FD"
$availBorder.ToolTip = "Native: $($assign.AvailableGroup)"
}
else {
$availBorder.Background = "Transparent"
}
$availText = New-Object System.Windows.Controls.TextBlock
$availText.Text = if ($assign.AvailableStatus -ne "None") { "✓" } else { "" }
$availText.FontSize = 10
$availText.HorizontalAlignment = "Center"
$availText.VerticalAlignment = "Center"
$availBorder.Child = $availText
[System.Windows.Controls.Grid]::SetColumn($availBorder, 1)
$rowGrid.Children.Add($availBorder) | Out-Null
# Required Indikator
$reqBorder = New-Object System.Windows.Controls.Border
$reqBorder.Width = 20
$reqBorder.Height = 16
$reqBorder.CornerRadius = "3"
$reqBorder.HorizontalAlignment = "Center"
if ($assign.RequiredStatus -eq "Full") {
$reqBorder.Background = "#C8E6C9"
$reqBorder.ToolTip = "Bereits Mitglied: $($assign.RequiredGroup)`n$($assign.RequiredTooltip)"
}
elseif ($assign.RequiredStatus -eq "Partial") {
$reqBorder.Background = "#FFF9C4"
$reqBorder.ToolTip = "Teilweise Mitglied: $($assign.RequiredGroup)`n$($assign.RequiredTooltip)"
}
elseif ($assign.RequiredStatus -eq "Native") {
$reqBorder.Background = "#E3F2FD"
$reqBorder.ToolTip = "Native: $($assign.RequiredGroup)"
}
else {
$reqBorder.Background = "Transparent"
}
$reqText = New-Object System.Windows.Controls.TextBlock
$reqText.Text = if ($assign.RequiredStatus -ne "None") { "✓" } else { "" }
$reqText.FontSize = 10
$reqText.HorizontalAlignment = "Center"
$reqText.VerticalAlignment = "Center"
$reqBorder.Child = $reqText
[System.Windows.Controls.Grid]::SetColumn($reqBorder, 2)
$rowGrid.Children.Add($reqBorder) | Out-Null
$pnlExistingAssignments.Children.Add($rowGrid) | Out-Null
# Trennlinie zwischen Apps hinzufügen (außer nach der letzten)
$rowIndex++
if ($rowIndex -lt $existingAssignments.Count) {
$rowSeparator = New-Object System.Windows.Controls.Border
$rowSeparator.BorderBrush = "#E0E0E0"
$rowSeparator.BorderThickness = "0,0,0,1"
$rowSeparator.Margin = "0,2,0,2"
$pnlExistingAssignments.Children.Add($rowSeparator) | Out-Null
}
}
}
function Update-UIForced {
$window.Dispatcher.Invoke([action]{}, [System.Windows.Threading.DispatcherPriority]::Render)
}
function Initialize-AppFilters {
$cmbAppFilter.Items.Clear()
$cmbAppFilter.Items.Add("Alle Apps")
$cmbAppFilter.Items.Add("--- Keine Zuweisungen ---")
$cmbAppFilter.Items.Add("Apps ohne Zuweisungen")
$cmbAppFilter.Items.Add("--- Nach Zuweisungstyp ---")
$cmbAppFilter.Items.Add("Apps nur mit Available-Gruppen")
$cmbAppFilter.Items.Add("Apps nur mit Required-Gruppen")
$cmbAppFilter.Items.Add("--- Mehrfache Zuweisungen ---")
$cmbAppFilter.Items.Add("Apps mit mehreren Available-Gruppen")
$cmbAppFilter.Items.Add("Apps mit mehreren Required-Gruppen")
$cmbAppFilter.Items.Add("--- Native Zuweisungen ---")
$cmbAppFilter.Items.Add("Apps mit 'All Users' in Available")
$cmbAppFilter.Items.Add("Apps mit 'All Users' in Required")
$cmbAppFilter.Items.Add("Apps mit 'All Devices' in Available")
$cmbAppFilter.Items.Add("Apps mit 'All Devices' in Required")
$cmbAppFilter.SelectedIndex = 0
}
function Set-GroupFilter {
if ($script:allGroups.Count -eq 0) { return }
$searchText = $txtGroupSearch.Text
# Prüfe ob der Text leer ist, der Platzhalter ist, oder nur Whitespace
$hasSearch = -not ([string]::IsNullOrWhiteSpace($searchText) -or $searchText -eq $txtGroupSearch.Tag)
$filtered = $script:allGroups
if ($hasSearch) {
# Suche nach dem Präfix "abt-hm-" + Suchtext (case-insensitive)
$filtered = @($filtered | Where-Object {
# Entferne das Präfix für die Suche
$deptName = $_.DisplayName -replace '^abt-hm-', ''
$deptName -ilike "*$searchText*"
})
}
# Sicherstellen, dass $filtered ein Array ist
if ($null -eq $filtered) { $filtered = @() }
if ($filtered -isnot [array]) { $filtered = @($filtered) }
# DataGrid aktualisieren mit Refresh
$script:groups = $filtered
$dgGroups.ItemsSource = $null
$dgGroups.ItemsSource = $script:groups
$dgGroups.Items.Refresh()
if ($hasSearch) {
$txtGroupCount.Text = "$($filtered.Count) von $($script:allGroups.Count) Gruppen (Suche: '$searchText')"
}
else {
$txtGroupCount.Text = "$($script:allGroups.Count) Gruppen geladen"
}
}
function Set-AppFilter {
if ($script:allApps.Count -eq 0) { return }
$searchText = $txtAppSearch.Text
# Prüfe ob der Text leer ist, der Platzhalter ist, oder nur Whitespace
$hasSearch = -not ([string]::IsNullOrWhiteSpace($searchText) -or $searchText -eq $txtAppSearch.Tag)
$filtered = $script:allApps
if ($hasSearch) {
# Case-insensitive Suche mit -ilike
$filtered = @($filtered | Where-Object { $_.AppName -ilike "*$searchText*" })
}
switch ($script:currentFilter) {
"Apps ohne Zuweisungen" {
$filtered = @($filtered | Where-Object { $_.AvailableGroupsCount -eq 0 -and $_.RequiredGroupsCount -eq 0 })
}
"Apps nur mit Available-Gruppen" {
$filtered = @($filtered | Where-Object { $_.AvailableGroupsCount -gt 0 -and $_.RequiredGroupsCount -eq 0 })
}
"Apps nur mit Required-Gruppen" {
$filtered = @($filtered | Where-Object { $_.RequiredGroupsCount -gt 0 -and $_.AvailableGroupsCount -eq 0 })
}
"Apps mit mehreren Available-Gruppen" {
$filtered = @($filtered | Where-Object { $_.AvailableGroups.Count -gt 1 })
}
"Apps mit mehreren Required-Gruppen" {
$filtered = @($filtered | Where-Object { $_.RequiredGroups.Count -gt 1 })
}
"Apps mit 'All Users' in Available" {
$filtered = @($filtered | Where-Object {
$hasAllUsers = $false
foreach ($group in $_.AvailableGroups) { if ($group.GroupId -eq "ALL_USERS") { $hasAllUsers = $true; break } }
$hasAllUsers
})
}
"Apps mit 'All Users' in Required" {
$filtered = @($filtered | Where-Object {
$hasAllUsers = $false
foreach ($group in $_.RequiredGroups) { if ($group.GroupId -eq "ALL_USERS") { $hasAllUsers = $true; break } }
$hasAllUsers
})
}
"Apps mit 'All Devices' in Available" {
$filtered = @($filtered | Where-Object {
$hasAllDevices = $false
foreach ($group in $_.AvailableGroups) { if ($group.GroupId -eq "ALL_DEVICES") { $hasAllDevices = $true; break } }
$hasAllDevices
})
}
"Apps mit 'All Devices' in Required" {
$filtered = @($filtered | Where-Object {
$hasAllDevices = $false
foreach ($group in $_.RequiredGroups) { if ($group.GroupId -eq "ALL_DEVICES") { $hasAllDevices = $true; break } }
$hasAllDevices
})
}
}
# Sicherstellen, dass $filtered ein Array ist
if ($null -eq $filtered) { $filtered = @() }
if ($filtered -isnot [array]) { $filtered = @($filtered) }
# DataGrid aktualisieren mit Refresh
$script:apps = $filtered
$dgApps.ItemsSource = $null
$dgApps.ItemsSource = $script:apps
$dgApps.Items.Refresh()
if ($hasSearch -or $script:currentFilter -ne "Alle Apps") {
$filterInfo = ""
if ($script:currentFilter -ne "Alle Apps") { $filterInfo = "Filter: $($script:currentFilter)" }
if ($hasSearch) {
if ($filterInfo) { $filterInfo += " | " }
$filterInfo += "Suche: '$searchText'"
}
$txtAppCount.Text = "$($filtered.Count) von $($script:allApps.Count) Apps ($filterInfo)"
}
else {
$txtAppCount.Text = "$($script:allApps.Count) Apps geladen"
}
}
function Get-AllGroupMembersRecursive {
param([string]$GroupId, [hashtable]$ProcessedGroups = @{}, [int]$Level = 0)
if ($ProcessedGroups.ContainsKey($GroupId)) { return @() }
$ProcessedGroups[$GroupId] = $true
$allUsers = @{}
$members = Get-GraphGroupMember -GroupId $GroupId -All
foreach ($member in $members) {
$memberType = $member.'@odata.type'
if ($memberType -eq "#microsoft.graph.user") {
try {
$user = Get-GraphUser -UserId $member.id -Property Id,DisplayName,UserPrincipalName
if ($user -and -not $allUsers.ContainsKey($user.id)) { $allUsers[$user.id] = $user }
} catch {}
}
elseif ($memberType -eq "#microsoft.graph.group") {
$subGroupUsers = Get-AllGroupMembersRecursive -GroupId $member.id -ProcessedGroups $ProcessedGroups -Level ($Level + 1)
foreach ($subUser in $subGroupUsers) {
if (-not $allUsers.ContainsKey($subUser.id)) { $allUsers[$subUser.id] = $subUser }
}
}
}
return $allUsers.Values
}
function Update-Summary {
$selectedTargets = @()
$targetType = ""
if ($tabMain.SelectedIndex -eq 0) {
if ($dgGroups.ItemsSource) {
foreach ($item in $dgGroups.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) { $selectedTargets += $item }
}
}
$targetType = "Gruppen"
}
else {
if ($dgUsers.ItemsSource) {
foreach ($item in $dgUsers.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) { $selectedTargets += $item }
}
}
$targetType = "Benutzer"
}
$assignmentCount = 0
if ($dgApps.ItemsSource) {
foreach ($item in $dgApps.ItemsSource) {
if ($null -ne $item) {
if ($item.UseAvailable -eq $true -and $null -ne $item.SelectedAvailableGroup -and $item.SelectedAvailableGroup.GroupId -ne "") { $assignmentCount++ }
if ($item.UseRequired -eq $true -and $null -ne $item.SelectedRequiredGroup -and $item.SelectedRequiredGroup.GroupId -ne "") { $assignmentCount++ }
}
}
}
# Session-Zuweisungen zählen
$sessionCount = $script:sessionAssignments.Count
# Zusammenfassung erstellen
$summaryParts = @()
if ($selectedTargets.Count -gt 0 -and $assignmentCount -gt 0) {
$currentOps = $selectedTargets.Count * $assignmentCount
$summaryParts += "Aktuell: $($selectedTargets.Count) $targetType × $assignmentCount Apps = $currentOps Ops"
}
if ($sessionCount -gt 0) {
$summaryParts += "Session: $sessionCount Zuweisung(en)"
}
if ($summaryParts.Count -gt 0) {
$txtSummary.Text = $summaryParts -join " | "
$btnApply.IsEnabled = $true
$btnApply.Background = "#0da075"
}
else {
if ($selectedTargets.Count -eq 0) { $txtSummary.Text = "Bitte wählen Sie mindestens eine Gruppe oder einen Benutzer aus" }
elseif ($assignmentCount -eq 0) { $txtSummary.Text = "Bitte aktivieren Sie mindestens eine App-Zuweisung" }
else { $txtSummary.Text = "Wählen Sie Ziele und Apps aus" }
$btnApply.IsEnabled = $false
$btnApply.Background = "LightGray"
}
}
function Show-CreateGroupDialog {
param($AppItem)
$dialogXaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Required-Gruppe erstellen" Height="575" Width="550"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Required-Gruppe erstellen" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
<TextBlock Grid.Row="1" Text="App: $($AppItem.AppName)" TextWrapping="Wrap" Margin="0,0,0,10"/>
<GroupBox Grid.Row="2" Header="Option 1: Automatischer Gruppenname" Margin="0,0,0,10" Padding="10">
<StackPanel>
<TextBlock Text="Vorgeschlagener Name:" Margin="0,0,0,5"/>
<TextBox Name="txtSuggestedName" IsReadOnly="True" Background="LightGray" Margin="0,0,0,5"/>
<TextBlock Name="txtSuggestedStatus" Text="" FontSize="10" Margin="0,0,0,10"/>
<Button Name="btnCreateSuggested" Content="Mit Vorschlag erstellen" Height="30"/>
</StackPanel>
</GroupBox>
<GroupBox Grid.Row="3" Header="Option 2: Eigener Gruppenname" Padding="10">
<StackPanel>
<TextBlock Text="Eigener Name (ohne Präfix):" Margin="0,0,0,5"/>
<TextBox Name="txtCustomName" Height="25" Margin="0,0,0,5"/>
<TextBlock Name="txtFullCustomName" Text="" FontSize="10" Foreground="Gray" Margin="0,0,0,5"/>
<TextBlock Name="txtCustomStatus" Text="" FontSize="10" Margin="0,0,0,10"/>
<Button Name="btnCreateCustom" Content="Mit eigenem Namen erstellen" Height="30" IsEnabled="False"/>
</StackPanel>
</GroupBox>
<GroupBox Grid.Row="4" Header="Optionen" Padding="10" Margin="0,0,0,10">
<CheckBox Name="chkAssignToApp" Content="Neue Gruppe direkt der App zuweisen (Required)" IsChecked="True"/>
</GroupBox>
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
<Button Name="btnCancel" Content="Abbrechen" Width="100" Height="30"/>
</StackPanel>
</Grid>
</Window>
"@
$dialogReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$dialogXaml)
$dialog = [Windows.Markup.XamlReader]::Load($dialogReader)
$dialog.Owner = $window
$txtSuggestedName = $dialog.FindName("txtSuggestedName")
$txtSuggestedStatus = $dialog.FindName("txtSuggestedStatus")
$btnCreateSuggested = $dialog.FindName("btnCreateSuggested")
$txtCustomName = $dialog.FindName("txtCustomName")
$txtFullCustomName = $dialog.FindName("txtFullCustomName")
$txtCustomStatus = $dialog.FindName("txtCustomStatus")
$btnCreateCustom = $dialog.FindName("btnCreateCustom")
$chkAssignToApp = $dialog.FindName("chkAssignToApp")
$btnCancel = $dialog.FindName("btnCancel")
$cleanedAppName = Clear-GroupName -Name $AppItem.AppName
$suggestedName = "intune-win-app-$cleanedAppName-required".ToLower()
$txtSuggestedName.Text = $suggestedName
try {
$existingGroups = Get-GraphGroup -Filter "displayName eq '$suggestedName'" -All
if ($existingGroups -and @($existingGroups).Count -gt 0) {
$txtSuggestedStatus.Text = "⚠ Gruppe existiert bereits!"
$txtSuggestedStatus.Foreground = "OrangeRed"
$btnCreateSuggested.IsEnabled = $false
}
else {
$txtSuggestedStatus.Text = "✓ Name verfügbar"
$txtSuggestedStatus.Foreground = "Green"
$btnCreateSuggested.IsEnabled = $true
}
}
catch {
$txtSuggestedStatus.Text = "Prüfung fehlgeschlagen"
$txtSuggestedStatus.Foreground = "Gray"
$btnCreateSuggested.IsEnabled = $false
}
$txtCustomName.Add_TextChanged({
if ([string]::IsNullOrWhiteSpace($txtCustomName.Text)) {
$txtFullCustomName.Text = ""
$txtCustomStatus.Text = ""
$btnCreateCustom.IsEnabled = $false
}
else {
$cleanedCustom = Clear-GroupName -Name $txtCustomName.Text
$fullName = "intune-win-app-$cleanedCustom-required".ToLower()
$txtFullCustomName.Text = "Vollständiger Name: $fullName"
try {
$existingGroups = Get-GraphGroup -Filter "displayName eq '$fullName'" -All
if ($existingGroups -and @($existingGroups).Count -gt 0) {
$txtCustomStatus.Text = "⚠ Gruppe existiert bereits!"
$txtCustomStatus.Foreground = "OrangeRed"
$btnCreateCustom.IsEnabled = $false
}
else {
$txtCustomStatus.Text = "✓ Name verfügbar"
$txtCustomStatus.Foreground = "Green"
$btnCreateCustom.IsEnabled = $true
}
}
catch {
$txtCustomStatus.Text = "Prüfung fehlgeschlagen"
$txtCustomStatus.Foreground = "Gray"
$btnCreateCustom.IsEnabled = $false
}
}
})
$btnCreateSuggested.Add_Click({
$dialog.Tag = @{
Action = "Create"
GroupName = $suggestedName
AssignToApp = $chkAssignToApp.IsChecked
}
$dialog.Close()
})
$btnCreateCustom.Add_Click({
$cleanedCustom = Clear-GroupName -Name $txtCustomName.Text
$fullName = "intune-win-app-$cleanedCustom-required".ToLower()
$dialog.Tag = @{
Action = "Create"
GroupName = $fullName
AssignToApp = $chkAssignToApp.IsChecked
}
$dialog.Close()
})
$btnCancel.Add_Click({ $dialog.Tag = @{ Action = "Cancel" }; $dialog.Close() })
$dialog.ShowDialog() | Out-Null
if ($dialog.Tag.Action -eq "Create") {
New-RequiredGroup -GroupName $dialog.Tag.GroupName -AppItem $AppItem -AssignToApp $dialog.Tag.AssignToApp
}
}
function Show-GroupMembersDialog {
param(
[string]$GroupId,
[string]$GroupName,
[bool]$ResolveNested
)
$dialogXaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Gruppenmitglieder: $GroupName" Height="550" Width="700"
WindowStartupLocation="CenterOwner">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Mitglieder von: $GroupName" FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10">
<CheckBox Name="chkResolveNestedDialog" Content="Verschachtelte Gruppen auflösen" VerticalAlignment="Center" Margin="0,0,15,0"/>
<Button Name="btnReloadMembers" Content="Neu laden" Width="90" Height="25"/>
<TextBlock Name="txtMemberCount" Text="" VerticalAlignment="Center" Margin="15,0,0,0" Foreground="Gray"/>
</StackPanel>
<DataGrid Name="dgMembers" Grid.Row="2" AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding DisplayName}" Width="*"/>
<DataGridTextColumn Header="UPN" Binding="{Binding UserPrincipalName}" Width="220"/>
<DataGridTextColumn Header="Typ" Binding="{Binding ObjectType}" Width="160"/>
<DataGridTextColumn Header="Quelle" Binding="{Binding SourceGroup}" Width="160"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
<Button Name="btnExportMembers" Content="Als CSV exportieren" Width="150" Height="30" Margin="0,0,10,0"/>
<Button Name="btnCloseMembers" Content="Schließen" Width="100" Height="30"/>
</StackPanel>
</Grid>
</Window>
"@
$dialogReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$dialogXaml)
$dialog = [Windows.Markup.XamlReader]::Load($dialogReader)
$dialog.Owner = $window
$chkResolveNestedDialog = $dialog.FindName("chkResolveNestedDialog")
$btnReloadMembers = $dialog.FindName("btnReloadMembers")
$txtMemberCount = $dialog.FindName("txtMemberCount")
$dgMembers = $dialog.FindName("dgMembers")
$btnExportMembers = $dialog.FindName("btnExportMembers")
$btnCloseMembers = $dialog.FindName("btnCloseMembers")
$chkResolveNestedDialog.IsChecked = $ResolveNested
$script:dialogMembers = @()
# Initiales Laden der Mitglieder
try {
$dialog.Cursor = [System.Windows.Input.Cursors]::Wait
$visited = [System.Collections.Generic.HashSet[string]]::new()
$resolve = $chkResolveNestedDialog.IsChecked -eq $true
$members = Get-ResolvedGroupMembers -GroupId $GroupId -SourceGroupName $GroupName -ResolveNested:$resolve -Visited $visited
$script:dialogMembers = @($members | Sort-Object ObjectType, DisplayName)
$dgMembers.ItemsSource = $script:dialogMembers
$txtMemberCount.Text = "$($script:dialogMembers.Count) Mitglied(er)"
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Laden der Mitglieder: $_", "Fehler", "OK", "Error")
}
finally {
$dialog.Cursor = [System.Windows.Input.Cursors]::Arrow
}
$btnReloadMembers.Add_Click({
try {
$dialog.Cursor = [System.Windows.Input.Cursors]::Wait
$visited = [System.Collections.Generic.HashSet[string]]::new()
$resolve = $chkResolveNestedDialog.IsChecked -eq $true
$members = Get-ResolvedGroupMembers -GroupId $GroupId -SourceGroupName $GroupName -ResolveNested:$resolve -Visited $visited
$script:dialogMembers = @($members | Sort-Object ObjectType, DisplayName)
$dgMembers.ItemsSource = $null
$dgMembers.ItemsSource = $script:dialogMembers
$txtMemberCount.Text = "$($script:dialogMembers.Count) Mitglied(er)"
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Laden der Mitglieder: $_", "Fehler", "OK", "Error")
}
finally {
$dialog.Cursor = [System.Windows.Input.Cursors]::Arrow
}
})
$btnExportMembers.Add_Click({
if (-not $script:dialogMembers -or $script:dialogMembers.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show("Keine Mitglieder zum Exportieren vorhanden.", "Hinweis", "OK", "Information")
return
}
$saveDialog = New-Object System.Windows.Forms.SaveFileDialog
$saveDialog.Filter = "CSV-Dateien (*.csv)|*.csv"
$safeName = ($GroupName -replace '[^\w\-]', '_')
$saveDialog.FileName = "GroupMembers_${safeName}_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
if ($saveDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$script:dialogMembers | Select-Object DisplayName, UserPrincipalName, ObjectType, SourceGroup |
Export-Csv -Path $saveDialog.FileName -NoTypeInformation -Encoding UTF8
[System.Windows.Forms.MessageBox]::Show("Export abgeschlossen:`n$($saveDialog.FileName)", "Export erfolgreich", "OK", "Information")
}
})
$btnCloseMembers.Add_Click({ $dialog.Close() })
$dialog.ShowDialog() | Out-Null
}
function Show-DepartmentSelectionDialog {
param($Departments, $SearchTerm)
$selectionXaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Abteilung auswählen" Height="450" Width="500"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="0,0,0,15">
<TextBlock Text="Mehrere Abteilungen gefunden" FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
<TextBlock Name="txtSearchInfo" TextWrapping="Wrap" FontSize="12"/>
</StackPanel>
<ListBox Grid.Row="1" Name="lstDepartments" FontSize="12" Margin="0,0,0,15">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" Padding="5"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Name="btnCancel" Content="Abbrechen" Width="100" Height="30" Margin="0,0,10,0"/>
<Button Name="btnSelect" Content="Auswählen" Width="100" Height="30" IsEnabled="False"/>
</StackPanel>
</Grid>
</Window>
"@
$selectionReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$selectionXaml)
$selectionDialog = [Windows.Markup.XamlReader]::Load($selectionReader)
$selectionDialog.Owner = $window
$txtSearchInfo = $selectionDialog.FindName("txtSearchInfo")
$lstDepartments = $selectionDialog.FindName("lstDepartments")
$btnCancel = $selectionDialog.FindName("btnCancel")
$btnSelect = $selectionDialog.FindName("btnSelect")
$txtSearchInfo.Text = "Es wurden $($Departments.Count) Abteilungen gefunden, die '$SearchTerm' enthalten. Bitte wählen Sie eine aus:"
foreach ($dept in $Departments) {
$lstDepartments.Items.Add($dept)
}
$lstDepartments.Add_SelectionChanged({
$btnSelect.IsEnabled = ($null -ne $lstDepartments.SelectedItem)
})
$btnCancel.Add_Click({
$selectionDialog.Tag = $null
$selectionDialog.Close()
})
$btnSelect.Add_Click({
$selectionDialog.Tag = $lstDepartments.SelectedItem
$selectionDialog.Close()
})
$selectionDialog.ShowDialog() | Out-Null
return $selectionDialog.Tag
}
function Show-AssignmentReviewDialog {
param($TargetsWithAssignments, $TargetType, $TotalAssignments)
$reviewXaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Zuweisungen überprüfen" Height="600" Width="900"
WindowStartupLocation="CenterOwner" ResizeMode="CanResize">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Bitte überprüfen Sie die geplanten Zuweisungen"
FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
<Grid Grid.Row="1" Margin="0,0,0,15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="#E8F5E9" Padding="10" Margin="0,0,5,0" CornerRadius="5">
<StackPanel>
<TextBlock Text="Ziele" FontWeight="Bold" FontSize="12" Margin="0,0,0,5"/>
<TextBlock Name="txtTargetsSummary" FontSize="14" FontWeight="Bold"/>
</StackPanel>
</Border>
<Border Grid.Column="1" Background="#E3F2FD" Padding="10" Margin="0,0,5,0" CornerRadius="5">
<StackPanel>
<TextBlock Text="Zuweisungen" FontWeight="Bold" FontSize="12" Margin="0,0,0,5"/>
<TextBlock Name="txtAssignmentsSummary" FontSize="14" FontWeight="Bold"/>
</StackPanel>
</Border>
<Border Grid.Column="2" Background="#FFF3E0" Padding="10" CornerRadius="5">
<StackPanel>
<TextBlock Text="Gesamt" FontWeight="Bold" FontSize="12" Margin="0,0,0,5"/>
<TextBlock Name="txtTotalOperations" FontSize="14" FontWeight="Bold"/>
</StackPanel>
</Border>
</Grid>
<GroupBox Grid.Row="2" Header="Detaillierte Übersicht" Margin="0,0,0,15">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="pnlDetails" Margin="5"/>
</ScrollViewer>
</GroupBox>
<TextBlock Grid.Row="3" Text="Möchten Sie diese Zuweisungen jetzt durchführen?"
FontSize="12" FontWeight="Bold" Margin="0,0,0,15" HorizontalAlignment="Center"/>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Name="btnCancel" Content="Abbrechen" Width="120" Height="35" Margin="0,0,10,0"/>
<Button Name="btnConfirm" Content="Ja, durchführen" Width="150" Height="35"
Background="#0da075" Foreground="White" FontWeight="Bold"/>
</StackPanel>
</Grid>
</Window>
"@
$reviewReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$reviewXaml)
$reviewDialog = [Windows.Markup.XamlReader]::Load($reviewReader)
$reviewDialog.Owner = $window
$txtTargetsSummary = $reviewDialog.FindName("txtTargetsSummary")
$txtAssignmentsSummary = $reviewDialog.FindName("txtAssignmentsSummary")
$txtTotalOperations = $reviewDialog.FindName("txtTotalOperations")
$pnlDetails = $reviewDialog.FindName("pnlDetails")
$btnCancel = $reviewDialog.FindName("btnCancel")
$btnConfirm = $reviewDialog.FindName("btnConfirm")
# Berechne Anzahl der einzigartigen Zuweisungen (Apps)
$uniqueAssignments = @{}
foreach ($target in $TargetsWithAssignments) {
foreach ($assign in $target.Assignments) {
$key = "$($assign.AppName)_$($assign.GroupId)_$($assign.Type)"
if (-not $uniqueAssignments.ContainsKey($key)) {
$uniqueAssignments[$key] = $assign
}
}
}
# Summary-Zahlen setzen
$txtTargetsSummary.Text = "$($TargetsWithAssignments.Count) $TargetType"
$txtAssignmentsSummary.Text = "$($uniqueAssignments.Count) Zuweisungen"
$txtTotalOperations.Text = "$TotalAssignments Operationen"
# Detaillierte Übersicht erstellen - jedes Target mit seinen EIGENEN Zuweisungen
foreach ($target in $TargetsWithAssignments) {
$targetBorder = New-Object System.Windows.Controls.Border
$targetBorder.Background = if ($target.Type -eq "Group") { "#E8F5E9" } else { "#E3F2FD" }
$targetBorder.Padding = "10"
$targetBorder.Margin = "0,0,0,10"
$targetBorder.CornerRadius = 3
$targetStack = New-Object System.Windows.Controls.StackPanel
# Header mit Icon basierend auf Target-Typ
$targetHeader = New-Object System.Windows.Controls.TextBlock
$icon = if ($target.Type -eq "Group") { "👥 " } else { "👤 " }
$targetHeader.Text = "$icon $($target.DisplayName)"
$targetHeader.FontWeight = "Bold"
$targetHeader.FontSize = 13
$targetHeader.Margin = "0,0,0,8"
$targetStack.Children.Add($targetHeader) | Out-Null
# NUR die Zuweisungen für DIESES Target anzeigen
foreach ($assignment in $target.Assignments) {
$assignmentGrid = New-Object System.Windows.Controls.Grid
$assignmentGrid.Margin = "15,0,0,3"
$col1 = New-Object System.Windows.Controls.ColumnDefinition
$col1.Width = "Auto"
$col2 = New-Object System.Windows.Controls.ColumnDefinition
$col2.Width = "*"
$col3 = New-Object System.Windows.Controls.ColumnDefinition
$col3.Width = "Auto"
$assignmentGrid.ColumnDefinitions.Add($col1) | Out-Null
$assignmentGrid.ColumnDefinitions.Add($col2) | Out-Null
$assignmentGrid.ColumnDefinitions.Add($col3) | Out-Null
$bulletIcon = New-Object System.Windows.Controls.TextBlock
if ($assignment.Type -eq "Available") {
$bulletIcon.Text = "● "
$bulletIcon.Foreground = "#4CAF50"
} else {
$bulletIcon.Text = "● "
$bulletIcon.Foreground = "#F44336"
}
$bulletIcon.FontSize = 11
$bulletIcon.VerticalAlignment = "Center"
[System.Windows.Controls.Grid]::SetColumn($bulletIcon, 0)
$assignmentGrid.Children.Add($bulletIcon) | Out-Null
$appText = New-Object System.Windows.Controls.TextBlock
$appText.Text = "$($assignment.AppName)"
$appText.FontSize = 11
$appText.Margin = "5,0,0,0"
$appText.VerticalAlignment = "Center"
[System.Windows.Controls.Grid]::SetColumn($appText, 1)
$assignmentGrid.Children.Add($appText) | Out-Null
$typeLabel = New-Object System.Windows.Controls.TextBlock
$typeLabel.Text = "$($assignment.Type)"
$typeLabel.FontSize = 10
$typeLabel.FontWeight = "Bold"
$typeLabel.Padding = "5,2"
$typeLabel.Margin = "5,0,0,0"
if ($assignment.Type -eq "Available") {
$typeLabel.Foreground = "#2E7D32"
} else {
$typeLabel.Foreground = "#C62828"
}
$typeLabel.VerticalAlignment = "Center"
[System.Windows.Controls.Grid]::SetColumn($typeLabel, 2)
$assignmentGrid.Children.Add($typeLabel) | Out-Null
$targetStack.Children.Add($assignmentGrid) | Out-Null
}
$separator = New-Object System.Windows.Controls.Border
$separator.Height = 1
$separator.Background = "#E0E0E0"
$separator.Margin = "0,8,0,0"
$targetStack.Children.Add($separator) | Out-Null
$countText = New-Object System.Windows.Controls.TextBlock
$countText.Text = "Σ $($target.Assignments.Count) Zuweisung(en) für dieses Ziel"
$countText.FontSize = 10
$countText.Foreground = "Gray"
$countText.Margin = "0,5,0,0"
$countText.FontStyle = "Italic"
$targetStack.Children.Add($countText) | Out-Null
$targetBorder.Child = $targetStack
$pnlDetails.Children.Add($targetBorder) | Out-Null
}
# DialogResult verwenden
$btnCancel.Add_Click({
$reviewDialog.DialogResult = $false
})
$btnConfirm.Add_Click({
$reviewDialog.DialogResult = $true
})
# Dialog anzeigen und Ergebnis speichern
$result = $reviewDialog.ShowDialog()
# Explizit boolean zurückgeben
if ($result -eq $true) {
return $true
}
else {
return $false
}
}
function New-RequiredGroup {
param($GroupName, $AppItem, $AssignToApp)
try {
$window.Cursor = [System.Windows.Input.Cursors]::Wait
$GroupName = $GroupName.ToLower()
$existingGroups = Get-GraphGroup -Filter "displayName eq '$GroupName'" -All
if ($existingGroups -and @($existingGroups).Count -gt 0) {
[System.Windows.Forms.MessageBox]::Show("Fehler: Eine Gruppe mit dem Namen '$GroupName' existiert bereits in Entra ID!", "Gruppe existiert", "OK", "Error")
return
}
$newGroup = New-GraphGroup -DisplayName $GroupName -MailEnabled $false -SecurityEnabled $true -MailNickname $GroupName
$successMessage = "Gruppe erfolgreich erstellt:`n`n$GroupName`n`nID: $($newGroup.id)"
if ($AssignToApp) {
try {
$assignmentBody = @{
"@odata.type" = "#microsoft.graph.mobileAppAssignment"
"intent" = "required"
"target" = @{
"@odata.type" = "#microsoft.graph.groupAssignmentTarget"
"groupId" = $newGroup.id
}
}
$assignmentUri = "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($AppItem.AppId)/assignments"
$jsonBody = $assignmentBody | ConvertTo-Json -Depth 10
Invoke-MgGraphRequest -Uri $assignmentUri -Method POST -Body $jsonBody -ContentType "application/json"
$newAssignment = New-Object IntuneGroupAssignment
$newAssignment.GroupId = $newGroup.id
$newAssignment.DisplayName = $GroupName
if ($AppItem.RequiredGroupsCount -eq 0) {
$AppItem.RequiredGroups.Clear()
$AppItem.RequiredGroups.Add($newAssignment)
$AppItem.SelectedRequiredGroup = $newAssignment
$AppItem.RequiredGroupsCount = 1
$AppItem.RequiredGroupDisplayText = $GroupName
}
else {
$AppItem.RequiredGroups.Add($newAssignment)
$AppItem.SelectedRequiredGroup = $newAssignment
$AppItem.RequiredGroupsCount = $AppItem.RequiredGroups.Count
}
$dgApps.Items.Refresh()
$successMessage += "`n`n✓ Gruppe wurde der App in Intune zugewiesen"
}
catch {
$successMessage += "`n`n⚠ Warnung: Gruppe wurde erstellt, aber Intune-Zuweisung fehlgeschlagen:`n$($_.Exception.Message)"
}
}
[System.Windows.Forms.MessageBox]::Show($successMessage, "Erfolg", "OK", "Information")
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Erstellen der Gruppe:`n`n$_", "Fehler", "OK", "Error")
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
}
}
function Invoke-Assignments {
param($Targets, $Assignments, $IsUserMode)
$pbProgress.Visibility = "Visible"
$btnApply.IsEnabled = $false
$totalOperations = $Targets.Count * $Assignments.Count
$currentOperation = 0
$successCount = 0
$errorCount = 0
$skippedCount = 0
foreach ($target in $Targets) {
foreach ($assignment in $Assignments) {
$currentOperation++
$pbProgress.Value = ($currentOperation / $totalOperations) * 100
$txtProgress.Text = "$($target.DisplayName) -> $($assignment.AppName) ($($assignment.GroupName) - $($assignment.Type))"
[System.Windows.Forms.Application]::DoEvents()
if ($assignment.GroupId -eq "ALL_USERS" -or $assignment.GroupId -eq "ALL_DEVICES") {
$skippedCount++
continue
}
try {
Add-GraphGroupMember -GroupId $assignment.GroupId -DirectoryObjectId $target.Id
$successCount++
}
catch {
if ($_.Exception.Message -like "*already exist*" -or $_.Exception.Message -like "*bereits vorhanden*" -or $_.Exception.Message -like "*One or more added object references already exist*") {
$successCount++
}
else {
$errorCount++
}
}
}
}
$pbProgress.Visibility = "Collapsed"
$btnApply.IsEnabled = $true
$txtProgress.Text = ""
# HTML-Report erstellen
$reportPath = New-AssignmentReport -Targets $Targets -Assignments $Assignments -IsUserMode $IsUserMode `
-SuccessCount $successCount -ErrorCount $errorCount -SkippedCount $skippedCount
$resultMessage = "Zuweisungen abgeschlossen:`n`nErfolgreich: $successCount`n"
if ($skippedCount -gt 0) { $resultMessage += "Übersprungen (Native): $skippedCount`n" }
$resultMessage += "Fehler: $errorCount`nGesamt: $totalOperations"
$resultMessage += "`n`nReport erstellt: $reportPath`n`nReport jetzt öffnen?"
$openReport = [System.Windows.Forms.MessageBox]::Show($resultMessage, "Abgeschlossen", "YesNo", "Information")
if ($openReport -eq "Yes") {
Start-Process $reportPath
}
if ($IsUserMode) {
foreach ($user in $script:users) { $user.IsSelected = $false }
$dgUsers.Items.Refresh()
}
else {
foreach ($group in $script:groups) { $group.IsSelected = $false }
$dgGroups.Items.Refresh()
}
foreach ($app in $script:apps) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
$dgApps.Items.Refresh()
Update-Summary
}
# ===== EVENT HANDLER =====
$btnConnect.Add_Click({
try {
$txtStatus.Text = "Lade Microsoft.Graph Module..."
$txtStatus.Foreground = "Yellow"
$window.Dispatcher.Invoke([action]{}, [System.Windows.Threading.DispatcherPriority]::Render)
# Module erst beim Connect laden
if (-not (Initialize-GraphModules)) {
$txtStatus.Text = "Module konnten nicht geladen werden"
$txtStatus.Foreground = "Red"
return
}
$txtStatus.Text = "Verbinde mit Microsoft Graph..."
$window.Dispatcher.Invoke([action]{}, [System.Windows.Threading.DispatcherPriority]::Render)
Connect-MgGraph -TenantId "739614a3-9800-4868-ba08-7e2c16128a9f" -ClientId "56b11df1-4228-44a1-b94f-9f3fc27678fc" -Scopes "Group.ReadWrite.All", "GroupMember.ReadWrite.All", "User.Read.All", "DeviceManagementApps.Read.All" -NoWelcome
$context = Get-MgContext
if ($context) {
$script:connected = $true
$txtStatus.Text = "Verbunden als: $($context.Account)"
$txtStatus.Foreground = "LightGreen"
$btnConnect.IsEnabled = $false
[System.Windows.Forms.MessageBox]::Show("Erfolgreich verbunden!", "Verbunden", "OK", "Information")
}
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Verbinden: $_", "Fehler", "OK", "Error")
$txtStatus.Text = "Verbindungsfehler"
$txtStatus.Foreground = "Red"
}
})
$btnLoadGroups.Add_Click({
if (-not $script:connected) {
[System.Windows.Forms.MessageBox]::Show("Bitte zuerst mit Graph verbinden!", "Nicht verbunden", "OK", "Warning")
return
}
# VOR dem Laden: Aktuelle Auswahlen speichern
Save-CurrentSelectionsToSession
try {
$window.Cursor = [System.Windows.Input.Cursors]::Wait
$allGroups = Get-GraphGroup -Filter "startswith(displayName,'abt-hm')" -All -Property Id,DisplayName
$script:allGroups = @()
foreach ($group in $allGroups) {
$groupItem = New-Object IntuneGroupItem
$groupItem.IsSelected = $false
$groupItem.Id = $group.id
$groupItem.DisplayName = $group.displayName
$script:allGroups += $groupItem
}
$script:groups = $script:allGroups
$dgGroups.ItemsSource = $null
$dgGroups.ItemsSource = $script:groups
$cmbDepartmentFilter.Items.Clear()
$cmbDepartmentFilter.Items.Add("-- Abteilung wählen --")
foreach ($group in $script:allGroups) { $cmbDepartmentFilter.Items.Add($group.DisplayName) }
$cmbDepartmentFilter.SelectedIndex = 0
$txtGroupCount.Text = "$($script:allGroups.Count) Gruppen geladen"
# WICHTIG: App-Checkboxen zurücksetzen bei neuem Laden
$script:groupModeAppSelections = @{}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
$dgApps.Items.Refresh()
}
# Target-Auswahlen aus Session wiederherstellen
Restore-SelectionsFromSession
Update-Summary
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Laden der Gruppen: $_", "Fehler", "OK", "Error")
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
}
})
# RPA-Gruppen laden (fest definierte Gruppen)
$btnLoadRpaGroups.Add_Click({
if (-not $script:connected) {
[System.Windows.Forms.MessageBox]::Show("Bitte zuerst mit Graph verbinden!", "Nicht verbunden", "OK", "Warning")
return
}
# VOR dem Laden: Aktuelle Auswahlen speichern
Save-CurrentSelectionsToSession
try {
$window.Cursor = [System.Windows.Input.Cursors]::Wait
# Die drei spezifischen RPA-Gruppen
$rpaGroupNames = @(
"intune-userrole-windowsclientuser-rpa",
"intune-userrole-windowsclientuser-rpa-fbt",
"intune-userrole-windowsclientuser-rpa-pro"
)
$script:rpaGroups = @()
foreach ($groupName in $rpaGroupNames) {
try {
$filter = "displayName eq '$groupName'"
$group = Get-GraphGroup -Filter $filter -Property Id,DisplayName
if ($group) {
$groupItem = New-Object IntuneGroupItem
$groupItem.IsSelected = $false
$groupItem.Id = $group.id
$groupItem.DisplayName = $group.displayName
$script:rpaGroups += $groupItem
}
}
catch {
Write-Host "Warnung: RPA-Gruppe '$groupName' nicht gefunden: $_"
}
}
$dgRpaGroups.ItemsSource = $null
$dgRpaGroups.ItemsSource = $script:rpaGroups
$txtRpaGroupCount.Text = "$($script:rpaGroups.Count) RPA-Gruppen geladen"
# WICHTIG: App-Checkboxen zurücksetzen bei neuem Laden
$script:rpaModeAppSelections = @{}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
$dgApps.Items.Refresh()
}
# Wiederherstellen von Session-Auswahlen
Restore-SelectionsFromSession
Update-Summary
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Laden der RPA-Gruppen: $_", "Fehler", "OK", "Error")
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
}
})
$btnLoadApps.Add_Click({
if (-not $script:connected) {
[System.Windows.Forms.MessageBox]::Show("Bitte zuerst mit Graph verbinden!", "Nicht verbunden", "OK", "Warning")
return
}
try {
$window.Cursor = [System.Windows.Input.Cursors]::Wait
$btnLoadApps.IsEnabled = $false
$pbProgress.Visibility = "Visible"
# Apps MIT Zuweisungen in einem Aufruf laden (viel schneller als separate Abfragen)
$uri = "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?`$expand=assignments"
$txtProgress.Text = "Lade Apps aus Intune..."
Update-UIForced
$response = Invoke-MgGraphRequest -Uri $uri -Method GET
$allApps = $response.value
while ($response.'@odata.nextLink') {
$response = Invoke-MgGraphRequest -Uri $response.'@odata.nextLink' -Method GET
$allApps += $response.value
}
$excludedTypes = @(
#iOS/iPadOS
'#microsoft.graph.iosLobApp',
'#microsoft.graph.iosStoreApp',
'#microsoft.graph.iosVppApp',
'#microsoft.graph.managedIOSLobApp',
'#microsoft.graph.managedIOSStoreApp',
# Android (inkl. Android Enterprise)
'#microsoft.graph.androidLobApp',
'#microsoft.graph.androidStoreApp',
'#microsoft.graph.managedAndroidLobApp',
'#microsoft.graph.managedAndroidStoreApp',
'#microsoft.graph.androidManagedStoreApp',
'#microsoft.graph.androidForWorkApp',
# macOS
'#microsoft.graph.macOSLobApp',
'#microsoft.graph.macOSDmgApp',
'#microsoft.graph.macOSPkgApp',
'#microsoft.graph.macOSOfficeSuiteApp',
'#microsoft.graph.macOSMicrosoftEdgeApp',
'#microsoft.graph.macOsVppApp'
)
$intuneApps = $allApps | Where-Object {
$appType = $_.'@odata.type'
$excludedTypes -notcontains $appType
}
$txtProgress.Text = "Sammle Gruppen-IDs..."
$pbProgress.Value = 30
Update-UIForced
# Alle eindeutigen Gruppen-IDs aus den Zuweisungen sammeln
$allGroupIds = @{}
foreach ($app in $intuneApps) {
if ($app.assignments) {
foreach ($assignment in $app.assignments) {
if ($assignment.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget') {
$allGroupIds[$assignment.target.groupId] = $true
}
}
}
}
# Alle Gruppen in einem Batch laden (viel schneller als einzelne Abfragen)
$txtProgress.Text = "Lade $($allGroupIds.Count) Gruppen..."
$pbProgress.Value = 40
Update-UIForced
$groupCache = @{}
if ($allGroupIds.Count -gt 0) {
$groupIdList = @($allGroupIds.Keys)
# Graph API Filter mit OR für mehrere IDs (max 15 pro Abfrage wegen URL-Länge)
for ($i = 0; $i -lt $groupIdList.Count; $i += 15) {
$batch = $groupIdList[$i..[Math]::Min($i + 14, $groupIdList.Count - 1)]
$filterParts = $batch | ForEach-Object { "id eq '$_'" }
$filter = $filterParts -join " or "
try {
$groups = Get-GraphGroup -Filter $filter -All -Property Id,DisplayName
foreach ($g in $groups) {
if ($g.id) { $groupCache[$g.id] = $g }
}
} catch { }
}
}
$txtProgress.Text = "Verarbeite Apps..."
$pbProgress.Value = 60
Update-UIForced
$script:allApps = @()
$totalApps = $intuneApps.Count
$processedApps = 0
foreach ($intuneApp in $intuneApps) {
$processedApps++
$appItem = New-Object IntuneAppItem
$appItem.AppName = $intuneApp.displayName
$appItem.AppId = $intuneApp.id
$appItem.UseAvailable = $false
$appItem.UseRequired = $false
$availableAssignments = @()
$requiredAssignments = @()
try {
# Zuweisungen sind bereits durch $expand=assignments geladen
$assignments = $intuneApp.assignments
foreach ($assignment in $assignments) {
$targetType = $assignment.target.'@odata.type'
$intent = $assignment.intent
$groupAssignment = New-Object IntuneGroupAssignment
if ($targetType -eq '#microsoft.graph.allLicensedUsersAssignmentTarget') {
$groupAssignment.GroupId = "ALL_USERS"
$groupAssignment.DisplayName = "All Users"
if ($intent -eq 'available') { $availableAssignments += $groupAssignment }
elseif ($intent -eq 'required') { $requiredAssignments += $groupAssignment }
}
elseif ($targetType -eq '#microsoft.graph.allDevicesAssignmentTarget') {
$groupAssignment.GroupId = "ALL_DEVICES"
$groupAssignment.DisplayName = "All Devices"
if ($intent -eq 'available') { $availableAssignments += $groupAssignment }
elseif ($intent -eq 'required') { $requiredAssignments += $groupAssignment }
}
elseif ($targetType -eq '#microsoft.graph.groupAssignmentTarget') {
$groupId = $assignment.target.groupId
# Gruppe aus vorab geladenem Cache holen
$cachedGroup = $groupCache[$groupId]
if ($null -ne $cachedGroup) {
# Filtere macOS-Gruppen aus (Gruppen mit "macos" im Namen)
if ($cachedGroup.displayName -like "*macos*") {
continue
}
# ALLE anderen zugewiesenen Gruppen anzeigen
$groupAssignment.GroupId = $groupId
$groupAssignment.DisplayName = $cachedGroup.displayName
if ($intent -eq 'available') { $availableAssignments += $groupAssignment }
elseif ($intent -eq 'required') { $requiredAssignments += $groupAssignment }
}
}
}
} catch { }
if ($availableAssignments.Count -eq 0) {
$appItem.AvailableGroupsCount = 0
$appItem.AvailableGroupDisplayText = "Keine Zuweisung vorhanden"
}
elseif ($availableAssignments.Count -eq 1) {
$appItem.AvailableGroups.Add($availableAssignments[0])
$appItem.SelectedAvailableGroup = $availableAssignments[0]
$appItem.AvailableGroupsCount = 1
$appItem.AvailableGroupDisplayText = $availableAssignments[0].DisplayName
}
else {
$appItem.AvailableGroups.Add((New-Object IntuneGroupAssignment -Property @{ GroupId = ""; DisplayName = "-- Bitte wählen --" }))
foreach ($avail in $availableAssignments) { $appItem.AvailableGroups.Add($avail) }
$appItem.SelectedAvailableGroup = $appItem.AvailableGroups[1]
$appItem.AvailableGroupsCount = $availableAssignments.Count + 1
}
if ($requiredAssignments.Count -eq 0) {
$appItem.RequiredGroupsCount = 0
$appItem.RequiredGroupDisplayText = "Keine Zuweisung vorhanden"
}
elseif ($requiredAssignments.Count -eq 1) {
$appItem.RequiredGroups.Add($requiredAssignments[0])
$appItem.SelectedRequiredGroup = $requiredAssignments[0]
$appItem.RequiredGroupsCount = 1
$appItem.RequiredGroupDisplayText = $requiredAssignments[0].DisplayName
}
else {
$appItem.RequiredGroups.Add((New-Object IntuneGroupAssignment -Property @{ GroupId = ""; DisplayName = "-- Bitte wählen --" }))
foreach ($req in $requiredAssignments) { $appItem.RequiredGroups.Add($req) }
$appItem.SelectedRequiredGroup = $appItem.RequiredGroups[1]
$appItem.RequiredGroupsCount = $requiredAssignments.Count + 1
}
$statusParts = @()
if ($availableAssignments.Count -gt 0) { $statusParts += "$($availableAssignments.Count)x AG" }
if ($requiredAssignments.Count -gt 0) { $statusParts += "$($requiredAssignments.Count)x RG" }
if ($statusParts.Count -gt 0) {
$appItem.StatusInfo = $statusParts -join " | "
$detailsParts = @()
if ($availableAssignments.Count -gt 0) { $detailsParts += "Available: " + (($availableAssignments | ForEach-Object { $_.DisplayName }) -join ", ") }
if ($requiredAssignments.Count -gt 0) { $detailsParts += "Required: " + (($requiredAssignments | ForEach-Object { $_.DisplayName }) -join ", ") }
$appItem.StatusDetails = $detailsParts -join "`n"
}
else {
$appItem.StatusInfo = "Keine"
$appItem.StatusDetails = "Diese App hat keine Zuweisungen"
}
$script:allApps += $appItem
if ($processedApps % 10 -eq 0) {
$progress = 30 + (($processedApps / $totalApps) * 60)
$pbProgress.Value = $progress
$txtProgress.Text = "Verarbeite: $processedApps / $totalApps Apps"
Update-UIForced
}
}
$script:apps = $script:allApps
$dgApps.ItemsSource = $null
$dgApps.ItemsSource = $script:apps
$txtAppCount.Text = "$($script:apps.Count) Apps geladen"
$pbProgress.Value = 100
$txtProgress.Text = "Abgeschlossen!"
Update-Summary
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Laden der Apps: $_", "Fehler", "OK", "Error")
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
$btnLoadApps.IsEnabled = $true
Start-Sleep -Milliseconds 500
$pbProgress.Visibility = "Collapsed"
$txtProgress.Text = ""
}
})
$btnCheckMembership.Add_Click({
if (-not $script:connected) {
[System.Windows.Forms.MessageBox]::Show("Bitte zuerst mit Graph verbinden!", "Nicht verbunden", "OK", "Warning")
return
}
# Prüfen ob Targets ausgewählt sind
$hasSelection = $false
switch ($tabMain.SelectedIndex) {
0 {
# Abteilungsgruppen-Tab
if ($dgGroups.ItemsSource) {
foreach ($item in $dgGroups.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$hasSelection = $true
break
}
}
}
}
1 {
# Einzelbenutzer-Tab
if ($dgUsers.ItemsSource) {
foreach ($item in $dgUsers.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$hasSelection = $true
break
}
}
}
}
2 {
# RPA-Tab
if ($dgRpaGroups.ItemsSource) {
foreach ($item in $dgRpaGroups.ItemsSource) {
if ($null -ne $item -and $item.IsSelected -eq $true) {
$hasSelection = $true
break
}
}
}
}
}
if (-not $hasSelection) {
[System.Windows.Forms.MessageBox]::Show("Bitte wählen Sie mindestens eine Gruppe oder einen Benutzer aus!", "Keine Auswahl", "OK", "Warning")
return
}
if (-not $dgApps.ItemsSource -or $script:allApps.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show("Bitte laden Sie zuerst die Apps!", "Keine Apps", "OK", "Warning")
return
}
try {
$window.Cursor = [System.Windows.Input.Cursors]::Wait
$btnCheckMembership.IsEnabled = $false
$txtProgress.Text = "Prüfe Gruppenmitgliedschaften..."
Update-UIForced
Update-MembershipIndicators
# Bestehende Zuweisungen im Panel anzeigen
Update-ExistingAssignmentsPanel
$txtProgress.Text = "Prüfung abgeschlossen"
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Prüfen der Mitgliedschaften: $_", "Fehler", "OK", "Error")
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
$btnCheckMembership.IsEnabled = $true
$txtProgress.Text = ""
}
})
$btnSearchUsers.Add_Click({
if (-not $script:connected) {
[System.Windows.Forms.MessageBox]::Show("Bitte zuerst mit Graph verbinden!", "Nicht verbunden", "OK", "Warning")
return
}
try {
$window.Cursor = [System.Windows.Input.Cursors]::Wait
$searchTerm = $txtUserSearch.Text
if ([string]::IsNullOrWhiteSpace($searchTerm) -or $searchTerm -eq $txtUserSearch.Tag) {
[System.Windows.Forms.MessageBox]::Show("Bitte geben Sie einen Suchbegriff ein!", "Keine Eingabe", "OK", "Warning")
return
}
# VOR der Suche: Aktuelle Auswahlen in Session speichern
Save-CurrentSelectionsToSession
$filter = "startswith(displayName,'$searchTerm') or startswith(userPrincipalName,'$searchTerm') or startswith(mail,'$searchTerm')"
$users = Get-GraphUser -Filter $filter -All -Property Id,DisplayName,UserPrincipalName,Mail
$script:users = @()
foreach ($user in $users) {
$userItem = New-Object IntuneUserItem
$userItem.IsSelected = $false
$userItem.Id = $user.id
$userItem.DisplayName = $user.displayName
$userItem.UserPrincipalName = $user.userPrincipalName
$script:users += $userItem
}
$dgUsers.ItemsSource = $null
$dgUsers.ItemsSource = $script:users
$txtUserCount.Text = "$($script:users.Count) Benutzer gefunden"
# ComboBox zurücksetzen wenn Suche verwendet wird
$cmbDepartmentFilter.SelectedIndex = 0
# WICHTIG: App-Checkboxen zurücksetzen bei neuer Suche
# Die bisherigen Zuweisungen sind in der Session gespeichert, aber neue Benutzer
# sollen mit leeren App-Auswahlen starten
$script:userModeAppSelections = @{}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
$dgApps.Items.Refresh()
}
# NACH der Suche: Target-Auswahlen aus Session wiederherstellen (aber NICHT App-Checkboxen)
Restore-SelectionsFromSession
Update-Summary
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Suchen der Benutzer: $_", "Fehler", "OK", "Error")
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
}
})
$btnClearUserSearch.Add_Click({
# VOR dem Löschen: Aktuelle Auswahlen speichern
Save-CurrentSelectionsToSession
# Suchfeld zurücksetzen
$txtUserSearch.Text = $txtUserSearch.Tag
# Benutzer-Liste leeren
$script:users = @()
$dgUsers.ItemsSource = $null
$txtUserCount.Text = "0 Benutzer geladen"
# ComboBox zurücksetzen
if ($cmbDepartmentFilter.Items.Count -gt 0) {
$cmbDepartmentFilter.SelectedIndex = 0
}
# App-Checkboxen zurücksetzen
$script:userModeAppSelections = @{}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
$dgApps.Items.Refresh()
}
Update-Summary
})
$btnSearchDept.Add_Click({
if (-not $script:connected) {
[System.Windows.Forms.MessageBox]::Show("Bitte zuerst mit Graph verbinden!", "Nicht verbunden", "OK", "Warning")
return
}
try {
$window.Cursor = [System.Windows.Input.Cursors]::Wait
$searchTerm = $txtDeptSearch.Text
if ([string]::IsNullOrWhiteSpace($searchTerm) -or $searchTerm -eq $txtDeptSearch.Tag) {
[System.Windows.Forms.MessageBox]::Show("Bitte geben Sie einen Suchbegriff ein!", "Keine Eingabe", "OK", "Warning")
return
}
# VOR der Suche: Aktuelle Auswahlen in Session speichern
Save-CurrentSelectionsToSession
# Suche nach Abteilungsgruppen die den Suchbegriff enthalten
$allDeptGroups = Get-GraphGroup -Filter "startswith(displayName,'abt-hm')" -All -Property Id,DisplayName
$matchingGroups = @($allDeptGroups | Where-Object {
$deptName = $_.displayName -replace '^abt-hm-', ''
$deptName -ilike "*$searchTerm*"
})
if ($matchingGroups.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show("Keine Abteilung gefunden mit: '$searchTerm'", "Keine Treffer", "OK", "Information")
return
}
# Bei mehreren Treffern: Auswahl anbieten
if ($matchingGroups.Count -gt 1) {
$selection = Show-DepartmentSelectionDialog -Departments $matchingGroups -SearchTerm $searchTerm
if ($selection -eq $null) { return }
$selectedGroup = $selection
}
else {
$selectedGroup = $matchingGroups[0]
}
# Lade alle Benutzer aus der ausgewählten Gruppe (rekursiv)
$pbProgress.Visibility = "Visible"
$txtProgress.Text = "Lade Mitglieder aus $($selectedGroup.DisplayName)..."
Update-UIForced
$allUsers = Get-AllGroupMembersRecursive -GroupId $selectedGroup.Id
$newUsers = New-Object System.Collections.ObjectModel.ObservableCollection[IntuneUserItem]
foreach ($user in $allUsers) {
$userItem = New-Object IntuneUserItem
$userItem.IsSelected = $false
$userItem.Id = $user.id
$userItem.DisplayName = $user.displayName
$userItem.UserPrincipalName = $user.userPrincipalName
$newUsers.Add($userItem)
}
$sortedUsers = $newUsers | Sort-Object DisplayName
$finalUsers = New-Object System.Collections.ObjectModel.ObservableCollection[IntuneUserItem]
foreach ($user in $sortedUsers) { $finalUsers.Add($user) }
$script:users = @($finalUsers)
$dgUsers.ItemsSource = $finalUsers
$txtUserCount.Text = "$($finalUsers.Count) Benutzer aus '$($selectedGroup.DisplayName)' (inkl. Untergruppen)"
# Andere Suchfelder zurücksetzen
$txtUserSearch.Text = $txtUserSearch.Tag
$cmbDepartmentFilter.SelectedIndex = 0
# WICHTIG: App-Checkboxen zurücksetzen bei neuer Abteilungssuche
$script:userModeAppSelections = @{}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
$dgApps.Items.Refresh()
}
# NACH der Suche: Target-Auswahlen aus Session wiederherstellen
Restore-SelectionsFromSession
Update-Summary
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler beim Suchen der Abteilung: $_", "Fehler", "OK", "Error")
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
$pbProgress.Visibility = "Collapsed"
$txtProgress.Text = ""
}
})
$btnClearDeptSearch.Add_Click({
# VOR dem Löschen: Aktuelle Auswahlen speichern
Save-CurrentSelectionsToSession
# Suchfeld zurücksetzen
$txtDeptSearch.Text = $txtDeptSearch.Tag
# Benutzer-Liste leeren
$script:users = @()
$dgUsers.ItemsSource = $null
$txtUserCount.Text = "0 Benutzer geladen"
# ComboBox zurücksetzen
if ($cmbDepartmentFilter.Items.Count -gt 0) {
$cmbDepartmentFilter.SelectedIndex = 0
}
# App-Checkboxen zurücksetzen
$script:userModeAppSelections = @{}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
$dgApps.Items.Refresh()
}
Update-Summary
})
$cmbDepartmentFilter.Add_SelectionChanged({
if (-not $script:connected) { return }
$window.Dispatcher.BeginInvoke([action]{
try {
$selectedDept = $cmbDepartmentFilter.SelectedItem.ToString()
if ($selectedDept -eq "-- Abteilung wählen --") {
$dgUsers.ItemsSource = $null
$txtUserCount.Text = "0 Benutzer geladen"
return
}
# VOR dem Laden: Aktuelle Auswahlen in Session speichern
Save-CurrentSelectionsToSession
$group = $script:groups | Where-Object { $_.DisplayName -eq $selectedDept } | Select-Object -First 1
if (-not $group) { return }
$window.Cursor = [System.Windows.Input.Cursors]::Wait
$allUsers = Get-AllGroupMembersRecursive -GroupId $group.Id
$newUsers = New-Object System.Collections.ObjectModel.ObservableCollection[IntuneUserItem]
foreach ($user in $allUsers) {
$userItem = New-Object IntuneUserItem
$userItem.IsSelected = $false
$userItem.Id = $user.id
$userItem.DisplayName = $user.displayName
$userItem.UserPrincipalName = $user.userPrincipalName
$newUsers.Add($userItem)
}
$sortedUsers = $newUsers | Sort-Object DisplayName
$finalUsers = New-Object System.Collections.ObjectModel.ObservableCollection[IntuneUserItem]
foreach ($user in $sortedUsers) { $finalUsers.Add($user) }
$script:users = @($finalUsers)
$dgUsers.ItemsSource = $finalUsers
$txtUserCount.Text = "$($finalUsers.Count) Benutzer (inkl. Untergruppen)"
# WICHTIG: App-Checkboxen zurücksetzen bei neuer Abteilungsauswahl
$script:userModeAppSelections = @{}
if ($dgApps.ItemsSource) {
foreach ($app in $dgApps.ItemsSource) {
if ($null -ne $app) {
$app.UseAvailable = $false
$app.UseRequired = $false
}
}
$dgApps.Items.Refresh()
}
# NACH dem Laden: Target-Auswahlen aus Session wiederherstellen
Restore-SelectionsFromSession
Update-Summary
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
}
}, [System.Windows.Threading.DispatcherPriority]::Background)
})
# Platzhalter-Text für Benutzersuche
$txtUserSearch.Add_GotFocus({
if ($txtUserSearch.Text -eq $txtUserSearch.Tag) {
$txtUserSearch.Text = ""
}
})
$txtUserSearch.Add_LostFocus({
if ([string]::IsNullOrWhiteSpace($txtUserSearch.Text)) {
$txtUserSearch.Text = $txtUserSearch.Tag
}
})
$txtUserSearch.Add_KeyDown({
if ($_.Key -eq 'Return') {
$btnSearchUsers.RaiseEvent([System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent))
}
})
# Platzhalter-Text für Abteilungssuche (im Einzelbenutzer-Tab)
$txtDeptSearch.Add_GotFocus({
if ($txtDeptSearch.Text -eq $txtDeptSearch.Tag) {
$txtDeptSearch.Text = ""
}
})
$txtDeptSearch.Add_LostFocus({
if ([string]::IsNullOrWhiteSpace($txtDeptSearch.Text)) {
$txtDeptSearch.Text = $txtDeptSearch.Tag
}
})
$txtDeptSearch.Add_KeyDown({
if ($_.Key -eq 'Return') {
$btnSearchDept.RaiseEvent([System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent))
}
})
# Platzhalter-Text für Abteilungssuche
$txtGroupSearch.Add_GotFocus({
if ($txtGroupSearch.Text -eq $txtGroupSearch.Tag) {
$txtGroupSearch.Text = ""
}
})
$txtGroupSearch.Add_LostFocus({
if ([string]::IsNullOrWhiteSpace($txtGroupSearch.Text)) {
$txtGroupSearch.Text = $txtGroupSearch.Tag
}
})
# Abteilungssuche Event Handler mit Debouncing
$script:groupSearchTimer = New-Object System.Windows.Threading.DispatcherTimer
$script:groupSearchTimer.Interval = [TimeSpan]::FromMilliseconds(300)
$script:groupSearchTimer.Add_Tick({
$script:groupSearchTimer.Stop()
if ($script:allGroups.Count -gt 0) {
Set-GroupFilter
}
})
$txtGroupSearch.Add_TextChanged({
# Bei jeder Änderung Timer neu starten (Debouncing)
$script:groupSearchTimer.Stop()
$script:groupSearchTimer.Start()
})
$btnClearGroupSearch.Add_Click({
$txtGroupSearch.Text = $txtGroupSearch.Tag
if ($script:allGroups.Count -gt 0) {
Set-GroupFilter
}
})
# Platzhalter-Text für App-Suche
$txtAppSearch.Add_GotFocus({
if ($txtAppSearch.Text -eq $txtAppSearch.Tag) {
$txtAppSearch.Text = ""
}
})
$txtAppSearch.Add_LostFocus({
if ([string]::IsNullOrWhiteSpace($txtAppSearch.Text)) {
$txtAppSearch.Text = $txtAppSearch.Tag
}
})
# App-Suche Event Handler mit Debouncing
$script:searchTimer = New-Object System.Windows.Threading.DispatcherTimer
$script:searchTimer.Interval = [TimeSpan]::FromMilliseconds(300)
$script:searchTimer.Add_Tick({
$script:searchTimer.Stop()
if ($script:allApps.Count -gt 0) {
Set-AppFilter
}
})
$txtAppSearch.Add_TextChanged({
# Bei jeder Änderung Timer neu starten (Debouncing)
$script:searchTimer.Stop()
$script:searchTimer.Start()
})
$btnClearAppSearch.Add_Click({
$txtAppSearch.Text = $txtAppSearch.Tag
if ($script:allApps.Count -gt 0) {
Set-AppFilter
}
})
$btnClearSession.Add_Click({
$result = [System.Windows.Forms.MessageBox]::Show(
"Möchten Sie wirklich alle Session-Zuweisungen löschen?`n`nDies setzt alle Checkboxen zurück.",
"Session löschen",
"YesNo",
"Question"
)
if ($result -eq "Yes") {
Clear-AllSessionAssignments
}
})
$cmbAppFilter.Add_SelectionChanged({
$selected = $cmbAppFilter.SelectedItem
if ($selected -eq $null) { return }
$selectedText = $selected.ToString()
if ($selectedText.StartsWith("---")) {
$cmbAppFilter.SelectedIndex = 0
return
}
$script:currentFilter = $selectedText
if ($script:allApps.Count -gt 0) {
Set-AppFilter
}
# Aktiven Filter anzeigen
if ($selectedText -ne "Alle Apps") {
$txtActiveFilter.Text = "Aktiv: $selectedText"
$txtActiveFilter.Visibility = "Visible"
$btnClearFilter.Visibility = "Visible"
} else {
$txtActiveFilter.Visibility = "Collapsed"
$btnClearFilter.Visibility = "Collapsed"
}
})
$btnClearFilter.Add_Click({
$cmbAppFilter.SelectedIndex = 0
$txtActiveFilter.Visibility = "Collapsed"
$btnClearFilter.Visibility = "Collapsed"
})
$dgApps.AddHandler([System.Windows.Controls.Button]::ClickEvent, [System.Windows.RoutedEventHandler]{
param($eventSender, $e)
if ($e.OriginalSource.Name -eq "btnCreateRequiredGroup") {
$appItem = $e.OriginalSource.Tag
if ($null -ne $appItem) {
Show-CreateGroupDialog -AppItem $appItem
}
}
})
$dgGroups.Add_CellEditEnding({
$window.Dispatcher.BeginInvoke({
Save-CurrentSelectionsToSession
Update-Summary
}, [System.Windows.Threading.DispatcherPriority]::Background)
})
$dgUsers.Add_CellEditEnding({
$window.Dispatcher.BeginInvoke({
Save-CurrentSelectionsToSession
Update-Summary
}, [System.Windows.Threading.DispatcherPriority]::Background)
})
$dgRpaGroups.Add_CellEditEnding({
$window.Dispatcher.BeginInvoke({
Save-CurrentSelectionsToSession
Update-Summary
}, [System.Windows.Threading.DispatcherPriority]::Background)
})
$dgApps.Add_CellEditEnding({
$window.Dispatcher.BeginInvoke({
Save-CurrentSelectionsToSession
Update-Summary
}, [System.Windows.Threading.DispatcherPriority]::Background)
})
$tabMain.Add_SelectionChanged({
# Prüfen ob sich der Tab wirklich geändert hat
$newTabIndex = $tabMain.SelectedIndex
if ($newTabIndex -eq $script:lastTabIndex) { return }
# Beim Tab-Wechsel: Aktuelle Auswahlen in Session speichern
Save-CurrentSelectionsToSession
# App-Auswahl für den ALTEN Tab speichern
Save-AppSelectionsForCurrentMode
# Tab-Index aktualisieren
$script:lastTabIndex = $newTabIndex
# App-Auswahl für den NEUEN Tab wiederherstellen
Restore-AppSelectionsForMode -TabIndex $newTabIndex
# Target-Auswahlen für den neuen Tab wiederherstellen
Restore-SelectionsFromSession
Update-Summary
})
$btnApply.Add_Click({
# Zuerst aktuelle UI-Auswahlen in Session speichern
Save-CurrentSelectionsToSession
# Prüfen ob Session-Zuweisungen vorhanden sind
if ($script:sessionAssignments.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show(
"Keine Zuweisungen vorhanden.`n`nBitte wählen Sie mindestens eine Gruppe/Benutzer und eine App-Zuweisung aus.",
"Keine Zuweisungen",
"OK",
"Warning"
)
return
}
# Session-Zuweisungen in das erwartete Format konvertieren
$targetsByType = @{
"Group" = @{}
"User" = @{}
}
foreach ($key in $script:sessionAssignments.Keys) {
$assignment = $script:sessionAssignments[$key]
$targetType = $assignment.TargetType
$targetId = $assignment.TargetId
if (-not $targetsByType[$targetType].ContainsKey($targetId)) {
$targetsByType[$targetType][$targetId] = @{
Id = $targetId
DisplayName = $assignment.TargetName
Assignments = @()
}
}
$targetsByType[$targetType][$targetId].Assignments += @{
AppName = $assignment.AppName
GroupId = $assignment.GroupId
GroupName = $assignment.GroupName
Type = $assignment.AssignmentType
}
}
# Alle Targets mit ihren spezifischen Zuweisungen sammeln
$allTargetsWithAssignments = @()
$targetTypeLabel = ""
$totalAssignments = 0
# Gruppen-Zuweisungen (jede Gruppe mit ihren eigenen Zuweisungen)
foreach ($targetId in $targetsByType["Group"].Keys) {
$target = $targetsByType["Group"][$targetId]
$allTargetsWithAssignments += @{
Id = $target.Id
DisplayName = $target.DisplayName
Type = "Group"
Assignments = $target.Assignments
}
$totalAssignments += $target.Assignments.Count
}
if ($targetsByType["Group"].Count -gt 0) { $targetTypeLabel = "Gruppen" }
# Benutzer-Zuweisungen (jeder Benutzer mit seinen eigenen Zuweisungen)
foreach ($targetId in $targetsByType["User"].Keys) {
$target = $targetsByType["User"][$targetId]
$allTargetsWithAssignments += @{
Id = $target.Id
DisplayName = $target.DisplayName
Type = "User"
Assignments = $target.Assignments
}
$totalAssignments += $target.Assignments.Count
}
if ($targetsByType["User"].Count -gt 0) {
if ($targetTypeLabel) { $targetTypeLabel += " & Benutzer" }
else { $targetTypeLabel = "Benutzer" }
}
# Zeige Review-Dialog mit target-spezifischen Zuweisungen
$confirmed = Show-AssignmentReviewDialog -TargetsWithAssignments $allTargetsWithAssignments -TargetType $targetTypeLabel -TotalAssignments $totalAssignments
# Nur ausführen wenn EXPLIZIT bestätigt wurde
if ($confirmed -eq $true) {
# Ausführung mit Session-Zuweisungen
Invoke-SessionAssignments
}
})
<#$btnApply.Add_Click({
Write-Host "`n========== DEBUG: btnApply Click Start ==========" -ForegroundColor Magenta
$selectedTargets = @()
$targetType = ""
if ($tabMain.SelectedIndex -eq 0) {
foreach ($item in $dgGroups.Items) {
if ($item.IsSelected -eq $true) {
$selectedTargets += $item
}
}
$targetType = "Abteilungsgruppen"
}
else {
foreach ($item in $dgUsers.Items) {
if ($item.IsSelected -eq $true) {
$selectedTargets += $item
}
}
$targetType = "Benutzer"
}
Write-Host "DEBUG: Targets = $($selectedTargets.Count), Type = $targetType" -ForegroundColor Yellow
$assignments = @()
foreach ($item in $dgApps.Items) {
if ($item.UseAvailable -eq $true -and $item.SelectedAvailableGroup -ne $null -and $item.SelectedAvailableGroup.GroupId -ne "") {
$assignments += @{
AppName = $item.AppName
GroupId = $item.SelectedAvailableGroup.GroupId
GroupName = $item.SelectedAvailableGroup.DisplayName
Type = "Available"
}
}
if ($item.UseRequired -eq $true -and $item.SelectedRequiredGroup -ne $null -and $item.SelectedRequiredGroup.GroupId -ne "") {
$assignments += @{
AppName = $item.AppName
GroupId = $item.SelectedRequiredGroup.GroupId
GroupName = $item.SelectedRequiredGroup.DisplayName
Type = "Required"
}
}
}
Write-Host "DEBUG: Assignments = $($assignments.Count)" -ForegroundColor Yellow
Write-Host "DEBUG: Rufe Show-AssignmentReviewDialog auf..." -ForegroundColor Yellow
# Zeige Review-Dialog
$confirmed = Show-AssignmentReviewDialog -Targets $selectedTargets -Assignments $assignments -TargetType $targetType
Write-Host "DEBUG: confirmed = $confirmed (Type: $($confirmed.GetType().Name))" -ForegroundColor Cyan
Write-Host "DEBUG: Teste: confirmed -eq `$true = $($confirmed -eq $true)" -ForegroundColor Cyan
# Nur ausführen wenn EXPLIZIT bestätigt wurde
if ($confirmed -eq $true) {
Write-Host "DEBUG: IF-Bedingung ist TRUE - führe Invoke-Assignments aus!" -ForegroundColor Green
Invoke-Assignments -Targets $selectedTargets -Assignments $assignments -IsUserMode ($tabMain.SelectedIndex -eq 1)
}
else {
Write-Host "DEBUG: IF-Bedingung ist FALSE - KEINE Ausführung!" -ForegroundColor Red
}
Write-Host "========== DEBUG: btnApply Click Ende ==========`n" -ForegroundColor Magenta
})#>
# ===== GRUPPENABFRAGE: Event Handler =====
$btnGroupQuerySearch.Add_Click({
if (-not $script:connected) {
[System.Windows.Forms.MessageBox]::Show("Bitte zuerst mit Graph verbinden!", "Nicht verbunden", "OK", "Warning")
return
}
$searchTerm = $txtGroupQuerySearch.Text.Trim()
if ([string]::IsNullOrWhiteSpace($searchTerm) -or $searchTerm -eq $txtGroupQuerySearch.Tag) {
[System.Windows.Forms.MessageBox]::Show("Bitte einen Suchbegriff eingeben.", "Hinweis", "OK", "Information")
return
}
try {
$window.Cursor = [System.Windows.Input.Cursors]::Wait
$found = Find-GraphGroupsByName -SearchTerm $searchTerm
$script:groupQueryResults = @()
foreach ($g in $found) {
$script:groupQueryResults += [PSCustomObject]@{
DisplayName = $g.displayName
Id = $g.id
}
}
$script:groupQueryResults = @($script:groupQueryResults | Sort-Object DisplayName)
$dgGroupQueryResults.ItemsSource = $null
$dgGroupQueryResults.ItemsSource = $script:groupQueryResults
$txtGroupQueryCount.Text = "$($script:groupQueryResults.Count) Gruppe(n) gefunden"
}
catch {
[System.Windows.Forms.MessageBox]::Show("Fehler bei der Gruppensuche: $_", "Fehler", "OK", "Error")
}
finally {
$window.Cursor = [System.Windows.Input.Cursors]::Arrow
}
})
$dgGroupQueryResults.Add_MouseDoubleClick({
$selected = $dgGroupQueryResults.SelectedItem
if (-not $selected) { return }
if (-not $script:connected) {
[System.Windows.Forms.MessageBox]::Show("Bitte zuerst mit Graph verbinden!", "Nicht verbunden", "OK", "Warning")
return
}
Show-GroupMembersDialog -GroupId $selected.Id -GroupName $selected.DisplayName -ResolveNested ($chkResolveNestedGroups.IsChecked -eq $true)
})
# Platzhalter-Text für Gruppenabfrage-Suche
$txtGroupQuerySearch.Add_GotFocus({
if ($txtGroupQuerySearch.Text -eq $txtGroupQuerySearch.Tag) {
$txtGroupQuerySearch.Text = ""
}
})
$txtGroupQuerySearch.Add_LostFocus({
if ([string]::IsNullOrWhiteSpace($txtGroupQuerySearch.Text)) {
$txtGroupQuerySearch.Text = $txtGroupQuerySearch.Tag
}
})
$txtGroupQuerySearch.Add_KeyDown({
if ($_.Key -eq 'Return') {
$btnGroupQuerySearch.RaiseEvent([System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent))
}
})
# Update-Timer für Summary und Session-Synchronisation
$script:updateTimer = New-Object System.Windows.Threading.DispatcherTimer
$script:updateTimer.Interval = [TimeSpan]::FromMilliseconds(300)
$script:updateTimer.Add_Tick({
Save-CurrentSelectionsToSession
Update-Summary
})
$window.Add_Loaded({ $script:updateTimer.Start() })
# ===== INITIALISIERUNG =====
# Setze Platzhalter-Text NACH der Registrierung der Event-Handler
$txtUserSearch.Text = $txtUserSearch.Tag
$txtDeptSearch.Text = $txtDeptSearch.Tag
$txtGroupSearch.Text = $txtGroupSearch.Tag
$txtAppSearch.Text = $txtAppSearch.Tag
$txtGroupQuerySearch.Text = $txtGroupQuerySearch.Tag
# Initialisiere Filter
Initialize-AppFilters
# Initialisiere Session-Panel
Update-SessionPanel
Write-Host "Starte Intune App-Zuweisungs-Manager v5.2.0..."
$window.ShowDialog() | Out-Null
# Cleanup
if ($script:updateTimer) { $script:updateTimer.Stop() }
if ($script:searchTimer) { $script:searchTimer.Stop() }
if ($script:groupSearchTimer) { $script:groupSearchTimer.Stop() }
if ($script:connected) { Disconnect-MgGraph }