diverse anpassungen

This commit is contained in:
2026-06-17 09:41:43 +01:00
parent eeabb1db31
commit c860ea9a10
9 changed files with 429 additions and 41 deletions
+157 -2
View File
@@ -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 -1
View File
@@ -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.
+56 -25
View File
@@ -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 &amp; 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 {
+111 -2
View File
@@ -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;