diverse anpassungen
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Intune App-Manager – Web Edition
|
||||
# Intune Manager – Web Edition
|
||||
|
||||
Modernes Web-Frontend fuer den Intune App-Zuweisungs-Manager.
|
||||
Modernes Web-Frontend fuer den Intune Manager.
|
||||
PowerShell startet einen lokalen HTTP-Server, der das Frontend ausliefert
|
||||
und die Microsoft Graph-Aufrufe ausfuehrt.
|
||||
|
||||
|
||||
@@ -16,4 +16,4 @@ REM Run.cmd
|
||||
REM Run.cmd -Port 8088
|
||||
REM Run.cmd -NoBrowser
|
||||
|
||||
start "Intune App-Manager" /min powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File "%~dp0Start.ps1" %*
|
||||
start "Intune Manager" /min powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File "%~dp0Start.ps1" %*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Intune App-Zuweisungs-Manager - Web Edition
|
||||
# Intune Manager - Web Edition
|
||||
# Startet einen lokalen HTTP-Server und oeffnet das moderne Web-Frontend.
|
||||
#
|
||||
# Aufruf:
|
||||
@@ -64,7 +64,7 @@ function Test-RequiredModules {
|
||||
$nuget = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue
|
||||
if (-not $nuget -or $nuget.Version -lt [version]"2.8.5.201") {
|
||||
Write-Host ""
|
||||
Write-Host "NuGet-PackageProvider fehlt — wird im User-Scope nachgezogen..." -ForegroundColor Yellow
|
||||
Write-Host "NuGet-PackageProvider fehlt — wird im User-Scope nachgezogen..." -ForegroundColor Yellow
|
||||
try {
|
||||
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force -ErrorAction Stop | Out-Null
|
||||
} catch {
|
||||
@@ -126,7 +126,7 @@ if (-not $SkipModuleCheck) {
|
||||
)
|
||||
if (-not (Test-RequiredModules -Required $required -AutoInstall:$AutoInstall)) {
|
||||
Write-Host ""
|
||||
Write-Host "Server-Start abgebrochen — benoetigte Module fehlen." -ForegroundColor Red
|
||||
Write-Host "Server-Start abgebrochen — benoetigte Module fehlen." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host ""
|
||||
@@ -144,7 +144,7 @@ $script:Settings = Read-Settings
|
||||
if ($TenantId) { $script:Settings.connection.tenantId = $TenantId }
|
||||
if ($ClientId) { $script:Settings.connection.clientId = $ClientId }
|
||||
|
||||
# Globale Konfiguration. TenantId/ClientId/Scopes spiegeln die Settings —
|
||||
# Globale Konfiguration. TenantId/ClientId/Scopes spiegeln die Settings —
|
||||
# Connect-Endpoint und Co. lesen weiterhin $script:Config, das nach jedem
|
||||
# Settings-Save aktualisiert wird.
|
||||
$script:Config = @{
|
||||
@@ -177,7 +177,7 @@ $script:BuildStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
Write-Host " Intune App-Zuweisungs-Manager - Web Edition" -ForegroundColor Cyan
|
||||
Write-Host " Intune Manager - Web Edition" -ForegroundColor Cyan
|
||||
Write-Host " Version: $script:ToolVersion" -ForegroundColor Green
|
||||
Write-Host " BUILD: $script:BuildStamp" -ForegroundColor DarkGray
|
||||
Write-Host "==============================================" -ForegroundColor Cyan
|
||||
@@ -185,7 +185,7 @@ Write-Host ""
|
||||
|
||||
# ============================================================
|
||||
# Alte Instanzen beenden: laeuft das Tool bereits, haelt der alte Prozess den
|
||||
# Port (HttpListener via http.sys) — ein zweiter Start scheitert dann still und
|
||||
# Port (HttpListener via http.sys) — ein zweiter Start scheitert dann still und
|
||||
# das alte Fenster bedient weiter mit altem Code. Darum alle anderen Start.ps1-
|
||||
# Prozesse DIESES Pfads vorher beenden, damit ein Neustart wirklich neu laedt.
|
||||
# ============================================================
|
||||
|
||||
@@ -55,6 +55,14 @@ function Invoke-ApiHandler {
|
||||
return Get-GroupMembersEndpoint -GroupId $matches[1]
|
||||
}
|
||||
|
||||
if ($Method -eq "POST" -and $Path -match "^/api/groups/([^/]+)/members$") {
|
||||
return Add-GroupMembersEndpoint -GroupId $matches[1] -Body $Body
|
||||
}
|
||||
|
||||
if ($Method -eq "DELETE" -and $Path -match "^/api/groups/([^/]+)/members/([^/]+)$") {
|
||||
return Remove-GroupMemberEndpoint -GroupId $matches[1] -UserId $matches[2]
|
||||
}
|
||||
|
||||
if ($Method -eq "GET" -and $Path -match "^/api/apps/([^/]+)/details$") {
|
||||
return Get-AppDetailsEndpoint -AppId $matches[1]
|
||||
}
|
||||
@@ -1191,6 +1199,91 @@ function Get-GroupMembersEndpoint {
|
||||
return @{ items = $items; count = $items.Count }
|
||||
}
|
||||
|
||||
function Add-GroupMembersEndpoint {
|
||||
param([string]$GroupId, $Body)
|
||||
$err = Test-Connected
|
||||
if ($err) { return $err }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($GroupId)) {
|
||||
return @{ __status = 400; error = "GroupId fehlt" }
|
||||
}
|
||||
if (-not $Body) {
|
||||
return @{ __status = 400; error = "Request-Body fehlt" }
|
||||
}
|
||||
|
||||
$ids = @()
|
||||
if ($Body.userIds) {
|
||||
$ids = @($Body.userIds | ForEach-Object { [string]$_ } | Where-Object { $_ })
|
||||
} elseif ($Body.userId) {
|
||||
$ids = @([string]$Body.userId)
|
||||
}
|
||||
if ($ids.Count -eq 0) {
|
||||
return @{ __status = 400; error = "userId oder userIds fehlt im Body" }
|
||||
}
|
||||
|
||||
$success = 0; $errors = 0
|
||||
$results = @()
|
||||
foreach ($uid in $ids) {
|
||||
try {
|
||||
Add-GraphMember -GroupId $GroupId -DirectoryObjectId $uid
|
||||
$success++
|
||||
$results += @{ userId = $uid; status = "Success" }
|
||||
Write-Host " [ADD MEMBER] GroupId=$GroupId UserId=$uid OK" -ForegroundColor Green
|
||||
} catch {
|
||||
$details = Get-GraphErrorFriendly -ErrorRecord $_
|
||||
if ($details.IsWarning) {
|
||||
$success++
|
||||
$results += @{ userId = $uid; status = "AlreadyMember"; message = $details.Friendly }
|
||||
Write-Host " [ADD MEMBER] $uid bereits Mitglied in $GroupId" -ForegroundColor DarkGreen
|
||||
} else {
|
||||
$errors++
|
||||
$results += @{ userId = $uid; status = "Error"; code = $details.Code; message = $details.Friendly }
|
||||
Write-Host " [ADD MEMBER] FAIL $uid -> $GroupId [$($details.Code)] $($details.Friendly)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:State.GroupMembers.ContainsKey($GroupId)) {
|
||||
$script:State.GroupMembers.Remove($GroupId)
|
||||
}
|
||||
|
||||
return @{
|
||||
ok = ($errors -eq 0)
|
||||
success = [int]$success
|
||||
errors = [int]$errors
|
||||
total = [int]$ids.Count
|
||||
results = $results
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-GroupMemberEndpoint {
|
||||
param([string]$GroupId, [string]$UserId)
|
||||
$err = Test-Connected
|
||||
if ($err) { return $err }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($GroupId) -or [string]::IsNullOrWhiteSpace($UserId)) {
|
||||
return @{ __status = 400; error = "GroupId und UserId erforderlich" }
|
||||
}
|
||||
|
||||
try {
|
||||
$null = Invoke-MgGraphRequest `
|
||||
-Uri "https://graph.microsoft.com/v1.0/groups/$GroupId/members/$UserId/`$ref" `
|
||||
-Method DELETE
|
||||
Write-Host " [REMOVE MEMBER] GroupId=$GroupId UserId=$UserId OK" -ForegroundColor Green
|
||||
} catch {
|
||||
$details = Get-GraphErrorFriendly -ErrorRecord $_
|
||||
$status = if ($details.Code -like '*404*') { 404 } elseif ($details.Code -like '*403*') { 403 } else { 500 }
|
||||
Write-Host " [REMOVE MEMBER] FAIL [$($details.Code)] $($details.Friendly)" -ForegroundColor Red
|
||||
return @{ __status = $status; error = $details.Friendly; code = $details.Code }
|
||||
}
|
||||
|
||||
if ($script:State.GroupMembers.ContainsKey($GroupId)) {
|
||||
$script:State.GroupMembers.Remove($GroupId)
|
||||
}
|
||||
|
||||
return @{ ok = $true; groupId = $GroupId; userId = $UserId }
|
||||
}
|
||||
|
||||
function Search-GroupsEndpoint {
|
||||
param($Query)
|
||||
$err = Test-Connected
|
||||
@@ -2689,7 +2782,7 @@ footer code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;bac
|
||||
</details>
|
||||
|
||||
<footer>
|
||||
Intune App-Zuweisungs-Manager · Web Edition <code>v$(_enc $script:ToolVersion)</code> · Build <code>$(_enc $script:BuildStamp)</code>
|
||||
Intune Manager · Web Edition <code>v$(_enc $script:ToolVersion)</code> · Build <code>$(_enc $script:BuildStamp)</code>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================
|
||||
// Intune App-Manager — Frontend
|
||||
// Intune Manager — Frontend
|
||||
// Workflow: Connect → Select Targets → Pick Apps → Apply
|
||||
// =============================================================
|
||||
|
||||
@@ -4188,6 +4188,9 @@ const GexState = {
|
||||
selectedGroup: null,
|
||||
members: null,
|
||||
memberFilter: '',
|
||||
activeTab: 'export',
|
||||
userSearchResults: [],
|
||||
selectedUsers: new Map(),
|
||||
};
|
||||
|
||||
function openGroupExportModal() {
|
||||
@@ -4196,6 +4199,9 @@ function openGroupExportModal() {
|
||||
GexState.selectedGroup = null;
|
||||
GexState.members = null;
|
||||
GexState.memberFilter = '';
|
||||
GexState.activeTab = 'export';
|
||||
GexState.userSearchResults = [];
|
||||
GexState.selectedUsers = new Map();
|
||||
document.getElementById('gexSearch').value = '';
|
||||
document.getElementById('gexMemberFilter').value = '';
|
||||
document.getElementById('gexSearchResults').innerHTML = '<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>';
|
||||
@@ -4203,10 +4209,33 @@ function openGroupExportModal() {
|
||||
document.getElementById('gexMembersTitle').textContent = 'Mitglieder';
|
||||
document.getElementById('gexStats').textContent = '';
|
||||
document.getElementById('gexExportBtn').disabled = true;
|
||||
// Add-Tab zurücksetzen
|
||||
document.getElementById('gexUserSearch').value = '';
|
||||
document.getElementById('gexUserResults').innerHTML = '<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>';
|
||||
document.getElementById('gexSelectedUsers').innerHTML = '<div class="gex-hint">Noch keine Benutzer ausgewählt.</div>';
|
||||
document.getElementById('gexSelectedCount').textContent = '0';
|
||||
document.getElementById('gexAddBtn').disabled = true;
|
||||
// Tab-Anzeige auf Export zurücksetzen
|
||||
_gexSwitchTab('export');
|
||||
document.getElementById('modalGroupExport').classList.remove('hidden');
|
||||
setTimeout(() => document.getElementById('gexSearch').focus(), 80);
|
||||
}
|
||||
|
||||
function _gexSwitchTab(tab) {
|
||||
GexState.activeTab = tab;
|
||||
document.querySelectorAll('.gex-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
|
||||
document.getElementById('gexPaneExport').classList.toggle('hidden', tab !== 'export');
|
||||
document.getElementById('gexPaneAdd').classList.toggle('hidden', tab !== 'add');
|
||||
document.getElementById('gexExportBtn').classList.toggle('hidden', tab !== 'export');
|
||||
document.getElementById('gexAddBtn').classList.toggle('hidden', tab !== 'add');
|
||||
document.getElementById('gexStats').textContent = '';
|
||||
if (tab === 'add') setTimeout(() => document.getElementById('gexUserSearch').focus(), 80);
|
||||
}
|
||||
|
||||
document.querySelectorAll('.gex-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => _gexSwitchTab(btn.dataset.tab));
|
||||
});
|
||||
|
||||
document.getElementById('btnGroupExport')?.addEventListener('click', openGroupExportModal);
|
||||
|
||||
// Suche mit Debounce
|
||||
@@ -4257,8 +4286,12 @@ async function _gexSelectGroup(id, name) {
|
||||
GexState.selectedGroup = { Id: id, DisplayName: name };
|
||||
GexState.members = null;
|
||||
GexState.memberFilter = '';
|
||||
document.getElementById('gexMemberFilter').value = '';
|
||||
_gexRenderResults(); // aktualisiert selected-Highlight
|
||||
_gexUpdateAddBtn();
|
||||
|
||||
if (GexState.activeTab !== 'export') return;
|
||||
|
||||
document.getElementById('gexMemberFilter').value = '';
|
||||
document.getElementById('gexMembersTitle').textContent = `Mitglieder — ${name}`;
|
||||
document.getElementById('gexStats').textContent = '';
|
||||
document.getElementById('gexExportBtn').disabled = true;
|
||||
@@ -4340,6 +4373,128 @@ function _gexRenderMembers() {
|
||||
if (GexState.memberFilter) _gexApplyFilter();
|
||||
}
|
||||
|
||||
// ---- Benutzer-Suche im Add-Tab ----
|
||||
|
||||
let _gexUserSearchTimer = null;
|
||||
document.getElementById('gexUserSearch')?.addEventListener('input', e => {
|
||||
clearTimeout(_gexUserSearchTimer);
|
||||
const q = e.target.value.trim();
|
||||
if (q.length < 2) {
|
||||
GexState.userSearchResults = [];
|
||||
document.getElementById('gexUserResults').innerHTML = '<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>';
|
||||
return;
|
||||
}
|
||||
document.getElementById('gexUserResults').innerHTML = '<div class="gex-loading"><div class="spinner" style="width:14px;height:14px;border-width:2px;"></div><span>Suche…</span></div>';
|
||||
_gexUserSearchTimer = setTimeout(() => _gexDoUserSearch(q), 350);
|
||||
});
|
||||
|
||||
async function _gexDoUserSearch(q) {
|
||||
try {
|
||||
const data = await api(`/api/users?q=${encodeURIComponent(q)}`);
|
||||
GexState.userSearchResults = data.items || [];
|
||||
_gexRenderUserResults();
|
||||
} catch (e) {
|
||||
document.getElementById('gexUserResults').innerHTML = `<div class="gex-error">Suche fehlgeschlagen: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function _gexRenderUserResults() {
|
||||
const el = document.getElementById('gexUserResults');
|
||||
if (!el) return;
|
||||
const items = GexState.userSearchResults;
|
||||
if (items.length === 0) {
|
||||
el.innerHTML = '<div class="gex-empty">Keine Benutzer gefunden.</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = items.map(u => {
|
||||
const already = GexState.selectedUsers.has(u.Id);
|
||||
return `<div class="gex-user-result-item ${already ? 'selected' : ''}"
|
||||
data-id="${escapeHtml(u.Id)}" data-name="${escapeHtml(u.DisplayName || '')}" data-upn="${escapeHtml(u.UserPrincipalName || '')}">
|
||||
<span class="gex-user-name">${escapeHtml(u.DisplayName || '—')}</span>
|
||||
<span class="gex-user-upn">${escapeHtml(u.UserPrincipalName || u.Mail || '')}</span>
|
||||
${already ? '<span class="gex-user-added">✓</span>' : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
el.querySelectorAll('.gex-user-result-item:not(.selected)').forEach(item => {
|
||||
item.addEventListener('click', () => _gexSelectUser({ Id: item.dataset.id, DisplayName: item.dataset.name, UserPrincipalName: item.dataset.upn }));
|
||||
});
|
||||
}
|
||||
|
||||
function _gexSelectUser(u) {
|
||||
if (GexState.selectedUsers.has(u.Id)) return;
|
||||
GexState.selectedUsers.set(u.Id, u);
|
||||
_gexRenderUserResults();
|
||||
_gexRenderSelectedUsers();
|
||||
_gexUpdateAddBtn();
|
||||
}
|
||||
|
||||
function _gexDeselectUser(id) {
|
||||
GexState.selectedUsers.delete(id);
|
||||
_gexRenderUserResults();
|
||||
_gexRenderSelectedUsers();
|
||||
_gexUpdateAddBtn();
|
||||
}
|
||||
|
||||
function _gexRenderSelectedUsers() {
|
||||
const el = document.getElementById('gexSelectedUsers');
|
||||
const countEl = document.getElementById('gexSelectedCount');
|
||||
if (!el) return;
|
||||
const users = [...GexState.selectedUsers.values()];
|
||||
countEl.textContent = users.length;
|
||||
if (users.length === 0) {
|
||||
el.innerHTML = '<div class="gex-hint">Noch keine Benutzer ausgewählt.</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = users.map(u => `
|
||||
<div class="gex-selected-chip">
|
||||
<span class="gex-selected-chip-name">${escapeHtml(u.DisplayName || '—')}</span>
|
||||
<span class="gex-selected-chip-upn">${escapeHtml(u.UserPrincipalName || '')}</span>
|
||||
<button class="gex-selected-chip-remove" data-id="${escapeHtml(u.Id)}" title="Entfernen">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
el.querySelectorAll('.gex-selected-chip-remove').forEach(btn => {
|
||||
btn.addEventListener('click', () => _gexDeselectUser(btn.dataset.id));
|
||||
});
|
||||
}
|
||||
|
||||
function _gexUpdateAddBtn() {
|
||||
const btn = document.getElementById('gexAddBtn');
|
||||
if (!btn) return;
|
||||
btn.disabled = !(GexState.selectedGroup && GexState.selectedUsers.size > 0);
|
||||
}
|
||||
|
||||
document.getElementById('gexAddBtn')?.addEventListener('click', async () => {
|
||||
if (!GexState.selectedGroup) { toast('Bitte zuerst eine Gruppe auswählen.', 'warn'); return; }
|
||||
if (GexState.selectedUsers.size === 0) { toast('Bitte mindestens einen Benutzer auswählen.', 'warn'); return; }
|
||||
|
||||
const btn = document.getElementById('gexAddBtn');
|
||||
btn.disabled = true;
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.innerHTML = '<div class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px;"></div>Wird hinzugefügt…';
|
||||
|
||||
const userIds = [...GexState.selectedUsers.keys()];
|
||||
try {
|
||||
const data = await api(`/api/groups/${encodeURIComponent(GexState.selectedGroup.Id)}/members`, {
|
||||
method: 'POST',
|
||||
body: { userIds },
|
||||
});
|
||||
if (data.errors > 0) {
|
||||
toast(`${data.success} von ${data.total} hinzugefügt, ${data.errors} Fehler.`, 'warn');
|
||||
} else {
|
||||
toast(`${data.success} Benutzer zu „${GexState.selectedGroup.DisplayName}" hinzugefügt.`, 'ok');
|
||||
GexState.selectedUsers.clear();
|
||||
_gexRenderSelectedUsers();
|
||||
document.getElementById('gexUserSearch').value = '';
|
||||
document.getElementById('gexUserResults').innerHTML = '<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
toast(`Fehler: ${e.message}`, 'err');
|
||||
} finally {
|
||||
btn.innerHTML = origHtml;
|
||||
_gexUpdateAddBtn();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('gexExportBtn')?.addEventListener('click', () => {
|
||||
if (!GexState.members?.users) return;
|
||||
const m = GexState.members;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Intune App-Manager — Hilfe
|
||||
# Intune Manager — Hilfe
|
||||
|
||||
Web-Frontend für Microsoft Intune. Lädt Apps und Gruppen aus deinem Tenant über Microsoft Graph und macht das Verwalten von Zuweisungen schneller als im Intune-Portal selbst.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Intune App-Manager</title>
|
||||
<title>Intune Manager</title>
|
||||
<script>
|
||||
// Theme vor erstem Render anwenden — verhindert Flash
|
||||
(function () {
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="brand">
|
||||
<div id="brandLogo" class="logo"><span class="logo-letter">I</span></div>
|
||||
<div class="brand-text">
|
||||
<div class="brand-name">Intune App-Manager</div>
|
||||
<div class="brand-name">Intune Manager</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -798,40 +798,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Gruppen-Export -->
|
||||
<!-- MODAL: Gruppen-Export / Benutzer hinzufügen -->
|
||||
<div id="modalGroupExport" class="modal hidden">
|
||||
<div class="modal-backdrop" data-close></div>
|
||||
<div class="modal-box modal-xl">
|
||||
<div class="modal-head">
|
||||
<h3>Gruppen-Export</h3>
|
||||
<h3>Gruppen-Verwaltung</h3>
|
||||
<button class="modal-close" data-close aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body gex-body">
|
||||
|
||||
<!-- Linke Spalte: Suche + Ergebnisliste -->
|
||||
<div class="gex-left">
|
||||
<div class="gex-section-title">Gruppe suchen</div>
|
||||
<div class="search-wrap" style="margin-bottom:10px;">
|
||||
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
<input type="search" id="gexSearch" class="input input-search" placeholder="Gruppenname eingeben…" autocomplete="off">
|
||||
</div>
|
||||
<div id="gexSearchResults" class="gex-results">
|
||||
<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>
|
||||
</div>
|
||||
<!-- Tab-Leiste -->
|
||||
<div class="gex-tab-bar">
|
||||
<button class="gex-tab active" data-tab="export">Mitglieder & Export</button>
|
||||
<button class="gex-tab" data-tab="add">Benutzer hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: aufgelöste Mitglieder -->
|
||||
<div class="gex-right">
|
||||
<div class="gex-section-title" id="gexMembersTitle">Mitglieder</div>
|
||||
<div class="search-wrap" style="margin-bottom:10px;">
|
||||
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
<input type="search" id="gexMemberFilter" class="input input-search" placeholder="Mitglieder filtern…" autocomplete="off">
|
||||
</div>
|
||||
<div id="gexMembersArea" class="gex-members-area">
|
||||
<div class="gex-hint">Gruppe auswählen um Mitglieder zu laden.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gex-columns">
|
||||
|
||||
<!-- Linke Spalte: Gruppensuche (geteilt) -->
|
||||
<div class="gex-left">
|
||||
<div class="gex-section-title">Gruppe suchen</div>
|
||||
<div class="search-wrap" style="margin-bottom:10px;">
|
||||
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
<input type="search" id="gexSearch" class="input input-search" placeholder="Gruppenname eingeben…" autocomplete="off">
|
||||
</div>
|
||||
<div id="gexSearchResults" class="gex-results">
|
||||
<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Export-Tab -->
|
||||
<div class="gex-right" id="gexPaneExport">
|
||||
<div class="gex-section-title" id="gexMembersTitle">Mitglieder</div>
|
||||
<div class="search-wrap" style="margin-bottom:10px;">
|
||||
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
<input type="search" id="gexMemberFilter" class="input input-search" placeholder="Mitglieder filtern…" autocomplete="off">
|
||||
</div>
|
||||
<div id="gexMembersArea" class="gex-members-area">
|
||||
<div class="gex-hint">Gruppe auswählen um Mitglieder zu laden.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Benutzer-hinzufügen-Tab -->
|
||||
<div class="gex-right hidden" id="gexPaneAdd">
|
||||
<div class="gex-section-title">Benutzer suchen</div>
|
||||
<div class="search-wrap" style="margin-bottom:10px;">
|
||||
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
<input type="search" id="gexUserSearch" class="input input-search" placeholder="Name, UPN oder E-Mail…" autocomplete="off">
|
||||
</div>
|
||||
<div id="gexUserResults" class="gex-results gex-add-results">
|
||||
<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>
|
||||
</div>
|
||||
<div class="gex-section-title" style="margin-top:12px;">
|
||||
Ausgewählt <span id="gexSelectedCount" class="count-badge">0</span>
|
||||
</div>
|
||||
<div id="gexSelectedUsers" class="gex-selected-area">
|
||||
<div class="gex-hint">Noch keine Benutzer ausgewählt.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<span id="gexStats" class="muted" style="margin-right:auto;"></span>
|
||||
@@ -840,6 +867,10 @@
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Als CSV exportieren
|
||||
</button>
|
||||
<button class="btn btn-primary hidden" id="gexAddBtn" disabled>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
|
||||
Zu Gruppe hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gruppen-Mitglieder · Intune App-Manager</title>
|
||||
<title>Gruppen-Mitglieder · Intune Manager</title>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* =============================================================
|
||||
Intune App-Manager — Cal.com-Aesthetik
|
||||
Intune Manager — Cal.com-Aesthetik
|
||||
Weißes Canvas, schwarze CTAs, Inter, dezente Hairlines.
|
||||
============================================================= */
|
||||
|
||||
@@ -4442,12 +4442,45 @@ html[data-theme="dark"] .numInputWrapper span.arrowDown {
|
||||
|
||||
.gex-body {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
min-height: 420px;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gex-tab-bar {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
background: var(--surface-soft);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gex-tab {
|
||||
padding: 10px 18px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
margin-bottom: -1px;
|
||||
transition: color var(--tx), border-color var(--tx);
|
||||
}
|
||||
.gex-tab:hover { color: var(--text); }
|
||||
.gex-tab.active {
|
||||
color: var(--text);
|
||||
border-bottom-color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gex-columns {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.gex-left {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
@@ -4593,6 +4626,82 @@ html[data-theme="dark"] .numInputWrapper span.arrowDown {
|
||||
}
|
||||
.gex-error { color: var(--error); }
|
||||
|
||||
.gex-add-results {
|
||||
max-height: 180px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gex-user-result-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 7px 12px;
|
||||
border-bottom: 1px solid var(--hairline-soft);
|
||||
cursor: pointer;
|
||||
transition: background var(--tx);
|
||||
font-size: 13px;
|
||||
}
|
||||
.gex-user-result-item:last-child { border-bottom: none; }
|
||||
.gex-user-result-item:hover { background: var(--surface-soft); }
|
||||
.gex-user-result-item.selected {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
.gex-user-added {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: var(--success);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gex-selected-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--r-md);
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.gex-selected-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px 6px 12px;
|
||||
border-bottom: 1px solid var(--hairline-soft);
|
||||
font-size: 13px;
|
||||
}
|
||||
.gex-selected-chip:last-child { border-bottom: none; }
|
||||
|
||||
.gex-selected-chip-name {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 100px;
|
||||
max-width: 200px;
|
||||
}
|
||||
.gex-selected-chip-upn {
|
||||
flex: 1;
|
||||
font-size: 11.5px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.gex-selected-chip-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
padding: 0 3px;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--tx);
|
||||
}
|
||||
.gex-selected-chip-remove:hover { color: var(--error); }
|
||||
|
||||
/* Kompakte Optik fuer beide Themes — passt besser zum Tool */
|
||||
.flatpickr-calendar {
|
||||
font-family: inherit;
|
||||
|
||||
Reference in New Issue
Block a user