diff --git a/src/PolicyIO.ps1 b/src/PolicyIO.ps1
index 618cc12..09bf75d 100644
--- a/src/PolicyIO.ps1
+++ b/src/PolicyIO.ps1
@@ -154,14 +154,35 @@ function Get-GraphPolicyList {
param([Parameter(Mandatory=$true)][string]$Type)
$cfg = Get-PolicyTypeConfig $Type
if (-not $cfg) { throw "Unbekannter Policy-Typ: $Type" }
- $raw = Get-GraphPaged -Uri "https://graph.microsoft.com/beta/deviceManagement/$($cfg.Collection)"
- $items = @()
+ # Zuweisungen gleich mitladen ($expand=assignments), damit die Liste je Policy
+ # Anzahl + aufgeloeste Ziele zeigen kann.
+ $raw = Get-GraphPaged -Uri "https://graph.microsoft.com/beta/deviceManagement/$($cfg.Collection)?`$expand=assignments"
+ $items = @()
+ $needGroups = @{} # eindeutige Gruppen-Ids ueber alle Policies (fuer 1 Bulk-Lookup)
foreach ($r in $raw) {
$id = Get-PolicyProp $r 'id'
if (-not $id) { continue }
$name = Get-PolicyProp $r $cfg.NameField
if (-not $name) { $name = Get-PolicyProp $r 'displayName' }
if (-not $name) { $name = Get-PolicyProp $r 'name' }
+
+ $asg = @()
+ foreach ($a in @(Get-PolicyProp $r 'assignments')) {
+ $t = Get-PolicyProp $a 'target'
+ if (-not $t) { continue }
+ $ot = [string](Get-PolicyProp $t '@odata.type')
+ $gid = [string](Get-PolicyProp $t 'groupId')
+ $entry = $null
+ if ($ot -like '*exclusionGroupAssignmentTarget') { $entry = [ordered]@{ mode = 'exclude'; kind = 'group'; groupId = $gid } }
+ elseif ($ot -like '*groupAssignmentTarget') { $entry = [ordered]@{ mode = 'include'; kind = 'group'; groupId = $gid } }
+ elseif ($ot -like '*allDevicesAssignmentTarget') { $entry = [ordered]@{ mode = 'include'; kind = 'allDevices'; groupId = '' } }
+ elseif ($ot -like '*allLicensedUsersAssignmentTarget') { $entry = [ordered]@{ mode = 'include'; kind = 'allUsers'; groupId = '' } }
+ if ($entry) {
+ if ($gid) { $needGroups[$gid] = $true }
+ $asg += $entry
+ }
+ }
+
$items += [ordered]@{
id = [string]$id
name = [string]$name
@@ -170,6 +191,25 @@ function Get-GraphPolicyList {
platform = Get-PolicyPlatformLabel -Raw $r -Type $cfg.Key
odataType = [string](Get-PolicyProp $r '@odata.type')
lastModifiedDateTime = Get-PolicyProp $r 'lastModifiedDateTime'
+ assignments = $asg
+ assignmentCount = @($asg).Count
+ }
+ }
+
+ # Gruppennamen in EINEM Bulk-Lookup aufloesen (bereits bekannte aus dem Cache).
+ $lookup = @{}
+ try {
+ foreach ($g in @($script:State.Groups)) { if ($g.Id) { $lookup[[string]$g.Id] = [string]$g.DisplayName } }
+ foreach ($g in @($script:State.RpaGroups)) { if ($g.Id) { $lookup[[string]$g.Id] = [string]$g.DisplayName } }
+ } catch {}
+ $unknown = [string[]]@($needGroups.Keys | ForEach-Object { [string]$_ } | Where-Object { $_ -and -not $lookup.ContainsKey($_) })
+ if ($unknown.Count -gt 0) { try { Resolve-GroupNamesBulk -Ids $unknown -Lookup $lookup } catch {} }
+ foreach ($it in $items) {
+ foreach ($a in @($it.assignments)) {
+ if ($a.kind -eq 'group') {
+ $gid = [string]$a.groupId
+ $a.groupName = if ($lookup.ContainsKey($gid)) { $lookup[$gid] } else { $gid }
+ }
}
}
return $items
diff --git a/www/app.js b/www/app.js
index 5fa54c4..c79d42d 100644
--- a/www/app.js
+++ b/www/app.js
@@ -6321,7 +6321,7 @@ function polFmtDate(iso) {
async function loadPolicies() {
const body = document.getElementById('polBody');
- body.innerHTML = `
Lade Policies… `;
+ body.innerHTML = ` Lade Policies… `;
// Drei Typen parallel laden; ein fehlschlagender Typ (z.B. fehlender Scope)
// soll die anderen nicht blockieren.
const endpoints = [
@@ -6368,7 +6368,7 @@ function renderPolicies() {
const list = polFiltered();
document.getElementById('polCount').textContent = list.length;
if (!list.length) {
- body.innerHTML = `${PolState.items.length ? 'Keine Policy passt zum Filter.' : 'Keine Policies gefunden.'} `;
+ body.innerHTML = `${PolState.items.length ? 'Keine Policy passt zum Filter.' : 'Keine Policies gefunden.'} `;
polUpdateSelCount();
return;
}
@@ -6380,9 +6380,19 @@ function renderPolicies() {
${escapeHtml(p.name || '—')}
${escapeHtml(p.typeLabel || p.type)}
${escapeHtml(p.platform || '—')}
+ ${p.assignmentCount > 0
+ ? `${p.assignmentCount} `
+ : '— '}
${polFmtDate(p.lastModifiedDateTime)}
`;
}).join('');
+ body.querySelectorAll('.pol-asg-badge').forEach(btn => {
+ btn.addEventListener('click', e => {
+ e.stopPropagation();
+ const p = PolState.items.find(x => polKey(x) === btn.dataset.key);
+ if (p) openPolAssignView(p);
+ });
+ });
body.querySelectorAll('.pol-row-check').forEach(cb => {
cb.addEventListener('change', () => {
if (cb.checked) PolState.selected.add(cb.dataset.key);
@@ -6702,6 +6712,8 @@ function openPolAssignModal(assignable) {
exportType: r.exportType || '',
include: [],
exclude: [],
+ allDevices: false,
+ allUsers: false,
}));
renderPolAssignList();
openModal('modalPolAssign');
@@ -6728,6 +6740,10 @@ function renderPolAssignList() {
`).join('')}
+
+ Alle Geräte
+ Alle Benutzer
+
`).join('');
PolAssignState.policies.forEach((_, i) => {
@@ -6753,10 +6769,18 @@ function renderPolAssignChips(idx, kind) {
function updatePolAssignInfo() {
const info = document.getElementById('polAssignInfo');
if (!info) return;
- const n = PolAssignState.policies.filter(p => p.include.length || p.exclude.length).length;
+ const n = PolAssignState.policies.filter(p => p.include.length || p.exclude.length || p.allDevices || p.allUsers).length;
info.textContent = n ? `${n} Policy(s) mit Zuweisung` : 'Keine Zuweisung gewählt';
}
+// Integrierte Ziele (Alle Geräte/Benutzer) pro Policy im Post-Import-Modal.
+document.getElementById('polAssignList')?.addEventListener('change', e => {
+ const dev = e.target.closest('.pol-assign-alldev');
+ const usr = e.target.closest('.pol-assign-allusr');
+ if (dev) { PolAssignState.policies[+dev.dataset.idx].allDevices = dev.checked; updatePolAssignInfo(); }
+ else if (usr) { PolAssignState.policies[+usr.dataset.idx].allUsers = usr.checked; updatePolAssignInfo(); }
+});
+
let _polAssignSearchTimer = null;
function polAssignHideDropdowns() {
document.querySelectorAll('.pol-assign-dropdown').forEach(d => { d.classList.add('hidden'); d.innerHTML = ''; });
@@ -6821,27 +6845,24 @@ document.addEventListener('click', e => {
document.getElementById('polAssignApply')?.addEventListener('click', async () => {
const items = PolAssignState.policies
- .filter(p => p.include.length || p.exclude.length)
+ .filter(p => p.include.length || p.exclude.length || p.allDevices || p.allUsers)
.map(p => ({
exportType: p.exportType,
id: p.id,
policyName: p.name,
include: p.include.map(g => g.id),
exclude: p.exclude.map(g => g.id),
+ allDevices: !!p.allDevices,
+ allUsers: !!p.allUsers,
+ mode: 'replace', // frisch importierte Policy hat noch keine Zuweisungen
}));
if (!items.length) { closeModal('modalPolAssign'); return; }
setLoading('Weise Gruppen zu…');
try {
const res = await api('/api/policies/assign', { method: 'POST', body: { items }, timeoutMs: 120000 });
- const results = res.results || [];
- const fail = results.filter(r => !r.success);
- if (fail.length) {
- const first = fail[0];
- toast(`${res.assignedCount || 0} zugewiesen, ${fail.length} fehlgeschlagen. z.B. "${first.policyName || '?'}": ${first.error || 'Fehler'}`, 'err', 'Zuweisung');
- } else {
- toast(`${res.assignedCount || 0} Policy(s) zugewiesen.`, 'ok', 'Zuweisung');
- }
closeModal('modalPolAssign');
+ showPolAssignResult(res.results || [], 'replace');
+ await loadPolicies();
} catch (e) {
toast('Zuweisung fehlgeschlagen: ' + e.message, 'err');
} finally {
@@ -6861,6 +6882,30 @@ const POL_TYPE_TO_EXPORT = {
const PolBulkAssign = { policies: [], include: [], exclude: [] };
let _polBulkSearchTimer = null;
+// Zuweisungen einer einzelnen Policy ansehen (aus der Listen-Spalte).
+function openPolAssignView(p) {
+ document.getElementById('polAssignViewName').innerHTML =
+ `${escapeHtml(p.name || '—')} ${escapeHtml(p.typeLabel || p.type)} `;
+ const body = document.getElementById('polAssignViewBody');
+ const asg = p.assignments || [];
+ if (!asg.length) {
+ body.innerHTML = 'Keine Zuweisungen.
';
+ } else {
+ const label = a => a.kind === 'allDevices' ? 'Alle Geräte'
+ : a.kind === 'allUsers' ? 'Alle Benutzer'
+ : (a.groupName || a.groupId);
+ const inc = asg.filter(a => a.mode === 'include');
+ const exc = asg.filter(a => a.mode === 'exclude');
+ const chip = (a, cls) => `${escapeHtml(label(a))} `;
+ body.innerHTML =
+ `Include (${inc.length}) `
+ + `
${inc.length ? inc.map(a => chip(a, '')).join('') : '— '}
`
+ + `Exclude (${exc.length}) `
+ + `
${exc.length ? exc.map(a => chip(a, 'exclude')).join('') : '— '}
`;
+ }
+ openModal('modalPolAssignView');
+}
+
function openPolBulkAssign() {
const sel = PolState.items.filter(p => PolState.selected.has(polKey(p)));
if (!sel.length) return;
@@ -6993,15 +7038,9 @@ document.getElementById('polBulkApply')?.addEventListener('click', async () => {
setLoading('Weise Gruppen zu…');
try {
const res = await api('/api/policies/assign', { method: 'POST', body: { items }, timeoutMs: 180000 });
- const results = res.results || [];
- const fail = results.filter(r => !r.success);
- if (fail.length) {
- const first = fail[0];
- toast(`${res.assignedCount || 0} zugewiesen, ${fail.length} fehlgeschlagen. z.B. "${first.policyName || '?'}": ${first.error || 'Fehler'}`, 'err', 'Zuweisung');
- } else {
- toast(`Gruppen ${mode === 'add' ? 'hinzugefügt' : 'ersetzt'}: ${res.assignedCount || 0} Policy(s).`, 'ok', 'Zuweisung');
- }
closeModal('modalPolBulkAssign');
+ showPolAssignResult(res.results || [], mode);
+ await loadPolicies(); // Zuweisungs-Zähler in der Liste aktualisieren
} catch (e) {
toast('Zuweisung fehlgeschlagen: ' + e.message, 'err');
} finally {
@@ -7009,6 +7048,28 @@ document.getElementById('polBulkApply')?.addEventListener('click', async () => {
}
});
+// Ergebnis einer (Bulk-)Zuweisung pro Policy anzeigen.
+function showPolAssignResult(results, mode) {
+ const ok = results.filter(r => r.success && !r.skipped).length;
+ const skip = results.filter(r => r.success && r.skipped).length;
+ const fail = results.filter(r => !r.success).length;
+ document.getElementById('polBulkResultSummary').innerHTML =
+ `Modus: ${mode === 'add' ? 'Hinzufügen' : 'Ersetzen'} — `
+ + `${ok} zugewiesen `
+ + (skip ? ` · ${skip} übersprungen ` : '')
+ + (fail ? ` · ${fail} fehlgeschlagen ` : '');
+ document.getElementById('polBulkResultBody').innerHTML = results.map(r => {
+ let badge, detail = '';
+ if (!r.success) { badge = 'Fehler '; detail = `${escapeHtml(r.error || 'Unbekannter Fehler')} `; }
+ else if (r.skipped) { badge = 'Übersprungen '; }
+ else { badge = 'Zugewiesen '; }
+ return `${badge}${escapeHtml(r.policyName || r.id || '—')} ${detail}
`;
+ }).join('');
+ openModal('modalPolBulkResult');
+ const anyFail = fail > 0;
+ toast(anyFail ? `${ok} zugewiesen, ${fail} fehlgeschlagen.` : `${ok} Policy(s) zugewiesen.`, anyFail ? 'warn' : 'ok', 'Zuweisung');
+}
+
document.getElementById('btnPolAssignGroups')?.addEventListener('click', openPolBulkAssign);
// Alle Policies als Snapshot in den konfigurierten Git-Ordner schreiben + committen.
diff --git a/www/index.html b/www/index.html
index f5566e8..dfb5d5c 100644
--- a/www/index.html
+++ b/www/index.html
@@ -640,11 +640,12 @@
Name
Typ
Plattform
+ Zuweisungen
Geändert
- Klicke auf „Neu laden" um Policies zu laden.
+ Klicke auf „Neu laden" um Policies zu laden.
@@ -1040,6 +1041,42 @@
+
+
+
+
+
+
Zuweisungen
+ ×
+
+
+
+
+
+
+
+
+
+
+
+
Ergebnis der Zuweisung
+ ×
+
+
+
+
+
+
diff --git a/www/styles.css b/www/styles.css
index 7b3acd2..5d84849 100644
--- a/www/styles.css
+++ b/www/styles.css
@@ -5579,6 +5579,7 @@ body.read-only .app-row {
.pol-assign-dropdown .opt:hover { background: var(--hover, rgba(255,255,255,.06)); }
.pol-assign-dropdown .opt .desc { font-size: 11px; color: var(--text-dim, #888); margin-top: 1px; }
.pol-assign-dropdown .msg { padding: 8px 10px; font-size: 12px; color: var(--text-dim, #888); }
+.pol-assign-builtin { margin-top: 10px; }
/* Bulk-Zuweisung: Policy-Liste + Modus */
.pol-bulk-names { display: flex; flex-wrap: wrap; gap: 6px; max-height: 120px; overflow-y: auto; }
@@ -5592,6 +5593,26 @@ body.read-only .app-row {
.pol-bulk-radio input { margin-top: 2px; }
.pol-bulk-builtin .lbl { display: block; font-size: 12px; font-weight: 600; margin-bottom: 6px; color: var(--text-dim, #888); }
.pol-bulk-check { display: inline-flex; align-items: center; gap: 6px; margin-right: 18px; font-size: 13px; cursor: pointer; }
+/* Zuweisungs-Spalte + Detailansicht */
+.pol-asg-badge {
+ min-width: 24px; padding: 1px 8px; border: 1px solid var(--hairline, #333);
+ border-radius: var(--r-pill, 999px); background: var(--bg3, var(--hover));
+ color: var(--ink, inherit); font-size: 12px; font-weight: 600; cursor: pointer;
+}
+.pol-asg-badge:hover { border-color: var(--accent, #60a5fa); color: var(--accent, #60a5fa); }
+.pol-av-group .lbl { display: block; font-size: 12px; font-weight: 600; margin-bottom: 6px; color: var(--text-dim, #888); }
+/* Ergebnis-Report der Zuweisung */
+.pol-result-list { max-height: 50vh; overflow-y: auto; }
+.pol-res-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--hairline-soft, rgba(255,255,255,.06)); font-size: 13px; }
+.pol-res-row:last-child { border-bottom: 0; }
+.pol-res-name { font-weight: 500; }
+.pol-res-msg { color: #f87171; font-size: 12px; margin-left: auto; text-align: right; }
+.pol-res-badge { flex: none; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-pill, 999px); }
+.pol-res-badge.ok { background: rgba(74,222,128,.15); color: #4ade80; }
+.pol-res-badge.err { background: rgba(248,113,113,.15); color: #f87171; }
+.pol-res-badge.skip { background: var(--hairline-soft); color: var(--muted, #999); }
+.pol-res-ok { color: #4ade80; }
+.pol-res-err { color: #f87171; }
/* =============================================================
Multi-Tenant: Topbar-Umschalter + Profil-Editor